diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15dda6fc3..6e828e67a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,14 @@ name: CI on: push: branches: [main] + # `main` AND the long-lived branches other PRs stack onto. A pull request is + # tested against the branch it will actually merge into, and a stacked PR + # targeting anything but `main` matched nothing here — so #730, thirty commits + # of SDK work, ran no unit tests, no build and no lint at all. The only signal + # it produced was the daemon cross-compile, and only because it touched + # `crates/`. A PR that cannot go red is not a reviewed PR. pull_request: - branches: [main] + branches: [main, feat/fp-cli] concurrency: group: ci-${{ github.ref }} @@ -263,6 +269,9 @@ jobs: # claiming >=3.10 and testing only one of them is how a 3.10 user finds the break. fp-cli: runs-on: ubuntu-latest + # a uv sync plus pytest across two interpreters; the bound exists so a stalled + # package mirror cannot hold a release for six hours (#726). + timeout-minutes: 10 defaults: run: working-directory: fp-cli @@ -334,6 +343,9 @@ jobs: # requires-python advertises is tested. failproofai-sdk: runs-on: ubuntu-latest + # the same, across five — every version requires-python advertises; the bound exists so a stalled + # package mirror cannot hold a release for six hours (#726). + timeout-minutes: 10 defaults: run: working-directory: sdk/python diff --git a/CHANGELOG.md b/CHANGELOG.md index 490e380f8..2b170179d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,12 @@ ### Features +- **`collector-health.json` reports delivery, not only capture.** The file answered "is each source producing events" and could not answer "is anything arriving". A source's job ends when it writes a batch into the spool — the POST, the server's verdict and the parking of what would not go all happen afterwards — and the SDK's batches have no source entry at all, because `failproofai-sdk` writes them into the spool from the user's own process. So a machine shipping nothing but SDK events wrote an empty, perfectly healthy-looking `sources` map whether ingest was storing every event or discarding all of them. Ingest answers `200` with `{"accepted":N,"skipped":M}` and the daemon deletes the batch either way, so one systematically malformed field discards everything the machine produced while every layer reports success — this release fixes two such fields — and the only trace was an ERROR line in the daemon's log, which on a real install is journald and nobody reads it until they already suspect a problem. The new `delivery` section carries counters the `Uploader` already kept: `accepted`, `skipped`, `batches_fully_skipped` and the timestamp of the last upload the server accepted. It is omitted rather than zeroed when there is no uploader, because all-zero counters and "this daemon has no credential" are different facts, and it reads through to the `Uploader` rather than copying at attach time, since that object outlives a supervised task restart and a snapshot would freeze the file at "nothing has happened yet" — which reads exactly like a healthy idle machine. (#730) + +- **The Python SDK gains native support for LangChain/LangGraph, CrewAI, LlamaIndex and Pydantic AI, on a real identity layer.** It was a capture surface you operated by hand: 15 emit methods, each requiring `session_id=` and `agent_id=`, and nothing propagating them — `SKILL.md` stated the absence of an ambient session as a deliberate contract, and `references/integration.md` shipped a ~60-line contextvars wrapper *as markdown for customers to paste into their own codebase*. `session()`, `agent()` and `tool_call()` bind identity on contextvars instead, under both `with` and `async with`, with `current()` and a `propagate()` that carries identity into a thread (contextvars do not). `session_id`/`agent_id` are now optional on all 15 methods, resolving from scope — existing call sites are untouched, which is why the golden wire-format bytes are unchanged. `instrument()` then wires the four frameworks, each a translation table over one shared `RunTracker` emitting only the existing 15 event types, so nothing fans out to the server, collector, CLI or stored schema. Measured against hand-written instrumentation on one identical task: 4 events and 4 types by hand, 14 events and 8 types from the adapter — the manual version reported one model request/response pair for a run that made two LLM calls, and zero tool events for a run whose point was calling a tool. That is the ceiling of the approach rather than carelessness: `graph.invoke()` is one call from outside, and the loop, the dispatch, the second round-trip and the per-node timings all happen inside it. AutoGen is deliberately absent — `autogen-core` last shipped 2025-09-30 and the live product is AG2, whose middleware has no global auto-instrument hook. Zero dependencies survives and its test got stronger: adapters resolve by string through `importlib` at call time, the source scan is scoped with a per-file allowlist, and a fresh interpreter now proves no framework reaches `sys.modules` on `import failproofai_sdk`. Verified live end to end for all four frameworks against a real model — every one of the 15 event types reaching the events store with promoted columns and `parent_id` nesting intact. Ported from FailproofAI/agenteye#503 and reconciled against the eleven SDK fixes made since that branch was cut. (#730) + +- **`request_id` on `model_request` and `model_response`.** The dashboard pairs model events on it, but no SDK method accepted one and no doc mentioned it — so every integration written to our own documentation emitted unpairable model events, `demo-agent/mock_agent.py` among them. Optional, and appended last in the ordered field list so an event omitting it serialises byte-for-byte as before: `test_wire_format.py` freezes those bytes, and ingest's dedup key hashes the canonical payload, so a reordering would stop retried batches collapsing and surface as duplicate rows rather than as an error. (#730) + - Give the canary box three images instead of one, and bake the commit into each. `failproofai-canary`, `failproofai-translate` and `failproofai-docs-audit` replace the single shared toolchain image, and each carries the checkout, its dependencies and its build products — the canary also carries a compiled `failproofaid`, which it used to cross-compile in a sibling rust container on every run. What happened at 02:00 and 11:00 in front of nobody — clone, fetch, checkout, `bun install`, two `bun build`s, a `cargo build` — happens in CI now, once per commit, where a failure is a red build rather than a night with no report. Installing the twelve vendor CLIs @latest deliberately stays at run time: that is the measurement, not setup. **Only the canary carries a docker client**, and only its cron line mounts the socket — the other two spawn nothing, and an image without the client cannot be talked into reaching the host daemon. That split is the reason for three images rather than one, and it is asserted rather than described. (#705) - Publish those images on **every** push to main, with no path filter — the exact inverse of the old rule, for the same reason it existed. Job scripts used to reach the box through a run-time clone, so only the baked layer needed rebuilding; with the tree baked in, any commit changes what the images should contain and a path filter would leave the box running last week's code with nothing saying so. The staleness that remains is answered rather than prevented: `FP_SHA` and `BUILT_AT` are baked in, the entrypoint turns them into an age, and a stale image says so at the top of its run **and in every Slack report it produces**. An unreadable build date counts as stale, because a job that cannot say how old its own code is should not be claiming freshness. The matrix is `fail-fast: false`, so one broken Dockerfile leaves the other two published rather than freezing all three. (#705) @@ -34,6 +40,56 @@ ### Fixes +- **`collector.hooks = false` silently stopped shipping the Python SDK's events.** `CollectorConfig::is_enabled()` gated the entire collector on `sessions || hooks`, and `collector_tasks()` returns early when it is false — so on a machine with a credential and both capture sources off, the daemon started no spool watcher and no sweeper, printed nothing at all, and every batch `failproofai-sdk` wrote into `custom-agents/events/` sat there forever. Those two settings gate the daemon's OWN sources (CLI session transcripts, hook activity) and each is checked again at the point its source is registered, so they were never what delivery should key off: the spool also carries events the user's own instrumented agents produced, and the watcher is the only thing that ships them. `hooks` defaults to true, which is why this stayed hidden — it takes turning hooks off, the one choice available to somebody who wants their instrumented agents shipped and nothing else, and then there is no error on either side and an unread spool looks exactly like an idle one. `is_enabled()` is now `ingest.is_some()`; an unconfigured machine still starts no thread and no runtime. Verified live: with `{"sessions":false,"hooks":false}` the daemon now logs `collector started tasks=3`, and a batch written before it came up was delivered by the sweeper. The reload end-to-end test that toggled `hooks` as a stand-in for disconnecting now toggles the **credential**, which is what `--disconnect` actually removes (`clearIngestCredential`) — a stronger test of the scenario it exists for — and a companion asserts the replacement behaviour: both capture sources off still starts the spool watcher, and still starts no hook source. (#730) + +- **`instrument()` called before the framework import said nothing and recorded nothing.** With no argument it instruments every framework already in `sys.modules`, so calling it above the `import langchain` line — the natural place to put a setup call, and where a "call this first" instruction lands people — finds nothing, installs nothing and returns `()`. The process then runs to completion with the SDK imported, the adapter "installed" and not one event emitted, raising nothing. The message naming the exact fix was already there, at `logger.debug`, which no default logging config displays: the single mistake that costs a user all of their telemetry was the one the SDK was silent about. It is a `logger.warning` now, and it fires only when somebody explicitly asked for instrumentation and got none. (#730) + +- **`SKILL.md` told readers `atexit` runs on `SIGTERM`. It does not.** The paragraph opened by calling `SIGTERM` "not exotic — it is every rolling deploy", every `docker stop`, every Kubernetes eviction, and then reassured them that "Python's default handler exits, so `atexit` *does* run". CPython installs no handler for `SIGTERM`: `signal.getsignal(SIGTERM)` is `SIG_DFL`, the OS terminates the process where it stands, and the atexit flush never runs. So the readers most likely to act on that paragraph — anyone deploying into a container — were told they were covered on the one exit path that drops the queue. Measured: a child queueing 20 events and sending itself `SIGTERM` writes **zero**. The text now says what happens, and ships the handler that fixes it (`flush_now()` then `sys.exit(128 + signum)`, which unwinds so an open `agent()` scope still emits its `agent_end`). Two tests execute both halves in a subprocess — the bare case must keep losing everything, so the day the SDK grows a handler of its own the recipe is flagged as obsolete rather than left standing. (#730) + +- **A comma in `environment` silently discarded every event the process emitted.** Ingest splits that field on commas to build its filter facets, so it skips any line containing one — the whole line, not the field — and answers `200 {"accepted":0,"skipped":N}`. The daemon deletes the delivered batch, and the run is simply not in the dashboard: no exception in the agent, nothing in its output, and an empty session list that looks exactly like an agent nobody ran. Measured against the running stack: `AGENTEYE_ENVIRONMENT="prod,eu"` — a wholly reasonable thing to type — produced `accepted:0, skipped:1`. `failproofaid` has always refused a comma in `collector.environment` for precisely this reason; the SDK writes the same field into every event and did not check. `configure(environment=...)` now raises naming the fix. The env var warns and falls back to `dev` rather than raising, because it is read lazily inside `to_dict()` on whatever event happens to be next: raising there would take the caller's agent down from a line of telemetry, and landing under a visibly wrong environment beats vanishing. (#730) + +- **A bare `llm.invoke()` recorded no model call at all.** A LangChain run with no parent is the session's root, and the adapter turned every root into an `agent_start`/`agent_end` pair — including a root whose own `run_type` is `chat_model`, which is what a direct `ChatOpenAI(...).invoke(...)` outside any graph produces. So that call emitted an agent span and **nothing else**: no `model_request`, no `model_response`, and therefore no model name, no input or output tokens and no latency, while the trace still looked populated and nothing raised. It is not an edge case — a classifier, a summariser and a one-shot rewrite are all shaped exactly like this, and a supervisor that delegates to graphs and then writes its own summary hits it on the summary. `_start_root` now also dispatches the leaf starter for a leaf-typed root and `_on_end` closes the leaf before the agent, so the pair lands inside its own span rather than after it — the dashboard closes the span at `agent_end` and anything later is attributed to nothing. Purely additive: a chain-typed root is untouched, and five tests cover it, four of which fail if the fix is reverted. (#730) + +- **Two LangChain runs that merely overlapped under one session id were read as an interrupt/resume, and one of them was silently deleted.** `_start_root` reused an open agent whenever the session already had one, because that is what a LangGraph resume looks like — but "the agent is still open" is equally true of two roots that simply run at the same time, and langchain-core opens one root run **per input** for `.batch()`. Any two requests carrying the same conversation id through the documented `failproofai_sdk_session_id` key do the same. The second root then got no `agent_start` at all, its work was relabelled with the first root's `agent_id`, the first root to finish closed the shared agent, and everything the other root emitted after that resolved to nothing and was dropped — a real model call, with its tokens and its latency, gone behind one `WARNING` nobody reads. Measured end to end against the running stack: two concurrent chains on one session landed **5 rows instead of 8**, and a `.batch()` of three landed 8 instead of 12, each with a single agent span. The discriminator is `open_pauses`: `_end_root` skips `agent_end` exactly when it is non-empty, which is the only way an agent outlives its root. Both directions are covered — the overlap case and a real interrupt/resume, which must still be one span. (#730) + +- **A failing top-level `tool.invoke()` or `llm.invoke()` counted as two failures.** `_on_end` returns straight after `_end_root` for a root run, so the line that marks a failure as owned by the span below it never ran for a root that was also a leaf. The exception was reported twice — once as `tool_result.error` / `model_response.error` and again as a standalone `error` event — and the server derives `is_error` from both, so `sessionSummary.errorCount` double-counted it while the identical failure one Runnable deeper counted once. Confirmed in ClickHouse: the same `ValueError`, raised at the top level and one level down, produced three error rows and two. The root-leaf branch now marks the failure as owned, exactly as the nested path does; a failure that no span below reported still gets its one standalone `error`, or it reaches no surface at all. (#730) + +- **`uninstrument()` went on recording forever when the trace env var was exported by somebody else.** A configure hook cannot be deregistered, so teardown is "make the hook produce nothing" — and neither lever did. Clearing the ContextVar only reaches contexts derived from the caller's, and the env var is unset only when `install()` was the one that set it, since it must not clobber an environment it did not write. With `FAILPROOFAI_SDK_TRACE_LANGCHAIN=1` exported by a Dockerfile or a CI job, `_configure` kept constructing a live zero-arg tracer per callback manager and a fully torn-down adapter recorded every event of every later run. Verified: a run issued **after** `uninstrument()` landed a complete four-event session in ClickHouse. There is now a module-level kill switch checked at the two entry points that gate everything else, so the promise holds however the hook was reached — and it is a switch, not a fuse: re-instrumenting works. (#730) + +- **`tool_result.output` was a Python repr of the envelope, and a tool that failed without raising was recorded as a success.** A tool handed the LLM's `ToolCall` dict — what `bind_tools` produces and what every modern tool loop passes — returns a `ToolMessage`, which has no JSON shape, so the single most-read field in a tool loop rendered as `ToolMessage(content='37000000', name='lookup_population', tool_call_id=…)` instead of `37000000`. The second half is worse: a `ToolMessage` carries `status="error"` when the framework converts the tool's exception into a message for the model instead of raising it, and `run.error` is empty on that path — so that failure had no representation anywhere, `is_error` 0 and a green span, with the text of it sitting in an output field nobody filters on. Both are read off the message now. (#730) + +- **Every LangChain entry on the Errors surface named its exception type twice.** `error` is the one event carrying `error_type` as its own field, and the server composes the row's `summary` as `": "`. The adapter fed it the same helper that fills `tool_result.error` and `agent_end.summary`, which prefixes the type because those two have nowhere else to say it — so every row read `ValueError: ValueError: denominator must be non-zero`. The CrewAI, LlamaIndex and Pydantic AI adapters all pass a bare `str(exc)` here; the fourth now agrees with them, and `agent_end.summary` deliberately still names the type, because nothing sits beside it. (#730) + +- **A stacked pull request ran no CI at all.** `ci.yml` triggers on `pull_request` into `main`, and #730 targets `feat/fp-cli` — so thirty commits of SDK work produced no unit run, no build, no lint and no docs check. The only signal it emitted was the daemon cross-compile, and only because it happened to touch `crates/`. A pull request that cannot go red is not a reviewed pull request. The trigger now also names `feat/fp-cli`, and turning it on immediately found what it was missing: the `fp-cli` and `failproofai-sdk` jobs carry no `timeout-minutes`, which main made mandatory in #726 and asserts in `release-pipeline.test.ts` — so #702 would have gone red on merge into main for a rule it had never been run against. Both jobs are bounded now. Also rewraps the daemon-skew warning in `fp-reset.ts`: "denies every tool call" is the consequence that message exists to state, and it was split across two hand-wrapped lines, so the test asserting the phrase failed against that branch of the message while the text read perfectly to a human. (#730) + +- **Five LangGraph bugs, one of them fabricating a human's approval.** `_node_of` decided which run was "the node" by comparing `run.name` to `metadata["langgraph_node"]` — and both sides of that are strings the user chooses. Three silent failures fell out of the one cause: `add_node("lookup_population", ToolNode([...]))` recorded **no `tool_use` or `tool_result` at all**, dropping the arguments, the result and the LLM's own `tool_call_id` and leaving two hook pairs where the tool should have been; `add_node("ChatOpenAI", ...)` recorded **no model call**, losing the model name, both token counts and the latency; and an inner runnable whose `run_name` matched the node key — or `sub.compile(name="child")` under `add_node("child", sub)` — emitted **two hook pairs per visit**, doubling node counts and halving apparent latency. Naming a node after the thing it runs is the obvious thing to type, and it was the thing that broke. A node's own run must now also be a non-leaf `run_type` carrying no `seq:step:` tag; both conditions are exclusions, so an upstream tag-convention change degrades to duplicate spans rather than to none. Worse than any of those: **a run that merely OVERLAPPED a pause was read as the resume.** `_start_root` treated "this session has an open pause and its agent is still open" as a continuation — a window that lasts as long as the human takes — so any other run carrying that session id inside it (a second request on one conversation id, a background summariser, a different graph) got no `agent_start`, had its nodes folded into the paused span, and emitted `agent_resume` + `human_input` **with an empty response**, closing the pause and reporting success. The dashboard showed an approval nobody gave. A resume must now also be shaped like one, which LangGraph makes checkable: an interrupted thread is continued only via `Command(...)` or `None`. And **a cross-process resume never closed the pause at all** — the real deployment shape, where one process interrupts and another approves: the second emitted nothing, so the session reported "still waiting on a human" forever and `pausedMs` never closed. The fix rests on three properties verified against the framework rather than assumed: `Interrupt.id` is `xxh3_128(checkpoint_ns)` and the interrupted task's namespace is byte-identical across the two invocations, so the second process reconstructs the id with no shared state; `on_resume` fires once per Pregel level, deepest last, which is what excludes a subgraph host; and only a level's first superstep re-runs interrupted tasks. Eighteen tests, including two that fail if the fix is pushed too far. (#730) + +- **Four Pydantic AI adapter bugs, two of them corrupting spans.** **A cancelled leaf closed after the agent it belongs to**, and sometimes not at all: `wrap_run` returns before `wrap_tool_execute` / `wrap_model_request` do on the cancellation path, because the graph awaits a gather of tool tasks and the run body unwinds the moment that future is cancelled, while each task's `CancelledError` lands a loop iteration later. Measured at `agent_end` .565709 against a matching `tool_result` at .566689 — and the dashboard closes the agent span at `agent_end`, so everything after it is attributed to nothing, which this adapter's own comments say three times. In some interleavings the ambient identity was already gone and the late leaf was dropped outright, leaving a `tool_use` with no result. Still-open leaves now close before `agent_end`, marked `fw_incomplete`. **`uninstrument()` during a live run emitted two `agent_end`s for one `agent_start`** — `cancelled` from teardown, then `success` from the run five seconds later, with the `tool_result` stranded between them. **`tool_result.output` was a Python repr of an envelope**: a tool returning `ToolReturn` recorded the whole repr, burying the answer beside `metadata` the model is documented never to see. And **a streamed `model_response.duration_ms` is the consumer's time** — on identical calls, 2556 ms with no consumer delay against 4059 ms with 1.5 s of sleep per delta, so 1503 ms of UI time sat inside the model's latency. No earlier hook is overridable without switching `agent.run()` into streaming mode, so the number cannot be made honest, only identifiable: `fw_streaming` now rides on the response as well as the request, since the request carries no `duration_ms` to exclude. (#730) + +- **A dataclass or a pydantic model recorded as a Python repr.** Neither is a `Mapping` or a `Sequence`, so both reached the branch that renders an object with no JSON shape — a tool's argument model, its structured return, a settings object on a request, all arriving as `Weather(city='Faro', celsius=21)`: a Python repr inside a JSON string, unreadable by `JSONExtract` and unfilterable on the dashboard. Every framework hands us these, and the adapters had begun solving it one at a time. The unwrap is deliberately shallow — `dataclasses.asdict` and `model_dump` both recurse and both copy, so on a large object they duplicate the whole tree before `_truncate` decides it only wanted the first 8 KB — and every part of it is guarded, because it runs the caller's own validators and properties; anything that raises falls back to `repr`, which is what happened before. `model_dump` rather than `dict`, because pydantic v2 names it distinctively while half the objects in a process have some attribute called `dict`, and a class is excluded explicitly, since `dataclasses.is_dataclass` is true of the class as well as its instances. (#730) + +- **Six CrewAI adapter bugs, four of them losing or misfiling events.** Found by driving real crews through a live gateway and reading the rows back out of the events store. **Hierarchical delegation was flattened**: `_tool_start` emitted `tool_use` but never noted the tool as a node, and CrewAI parents a delegated coworker's whole `AgentExecutionStartedEvent` on the `delegate_work_to_coworker` *tool* event — so the parent lookup missed it and fell back to the most recent root, putting the manager and both coworkers side by side under the crew instead of nesting coworker → manager → crew. **`FlowFailedEvent` was not in the translation table**, and a Flow whose method raises emits it and never emits `FlowFinishedEvent` — so the flow's `agent_start` was never closed and the session read `ongoing` forever, permanently in a long-lived process. **A Crew kicked off inside a Flow method became a second session**, because the flow-method span was not noted either, so `on_crew_started` read "no parent" and minted a new root: one logical run, two unlinked sessions, no `parent_id` on either. **Any event whose parent span was gone leaked into another run** through a process-global "most recent root" fallback — reproduced with two crews open, where an orphan tool emitted from crew Alpha's thread was recorded against crew Bravo; root selection now matches the ambient session. **`Task(human_input=True)` recorded nothing at all**: CrewAI has two HITL surfaces and only the Flow `@human_feedback` one is on the event bus, so the entire human wait was billed as active agent time — a real 38-second wait now measures as `agent_resume.duration_ms = 37878` inside a ~43-second agent span. That is the adapter's only patch, wrapping the narrowest seam, restoring the `staticmethod` descriptor on `uninstrument()` with an identity check and re-raising `KeyboardInterrupt` verbatim. And **`Agent.kickoff()` had no agent span at all** — the three LiteAgent events were unmapped, so with no ambient session it recorded zero rows and with one it filed everything under `agent_id="main"`. Thirteen tests, each fix reverted individually to prove the matching one fails. (#730) + +- **Three LlamaIndex adapter bugs, one flattening whole crews.** `AgentWorkflow` does not run its agents as nested workflows, so attribution from the span tree alone **collapsed a two-agent crew into a single `agent_id`** — 382 events under `"AgentWorkflow"` in the audited run, with the real names reachable only through `fw_agent_name`, a payload extra and not a groupable column. Each distinct `current_agent_name` now opens a nested agent under the workflow; the name is sticky because a `ToolCall` step carries none, and a guard stops a standalone `FunctionAgent` nesting inside itself. **A user-cancelled run was reported `outcome="success"`**: `cancel_run()` does not drop the span, the runtime catches its own `WorkflowCancelledByUser` and exits cleanly with `result=None`. Rather than infer cancellation from a null result, the adapter reads the framework's own `SpanCancelledEvent`, and the run closes `cancelled` with no `error`, because a stop button is not a failure. That event sits outside `_HANDLED_EVENTS` — the drift test walks only `llama_index.core.instrumentation.events.*` — so it ships with a drift guard of its own that fails if the class is renamed, moves, or loses `span_id`. And **a failed `agent_end` carried no `summary`**, the promoted column where a run's outcome is read; the reason lived only on the failing step's `hook_completed` payload and vanished entirely under `steps=False`. (#730) + +- **A tool's declared schema reached the events store as a Python repr.** `_core._truncate` and `_size` dispatched on the concrete `dict`, `list` and `tuple`, which misses every mapping a framework actually hands us that is not literally a dict — `MappingProxyType`, which is what `model_json_schema()` and any frozen config returns, and `ChainMap` among them — so those fell through to the branch that renders an object with no JSON shape via `repr`. A crewai `model_request` carried `tools[0].function.parameters.properties.from_unit = "{'title': 'From Unit', 'type': 'string'}"`: valid JSON holding a Python repr, so `JSONExtract` over it returns nothing and the field is unqueryable rather than merely ugly — and a tool's schema is exactly what you open a `model_request` to read. Both functions dispatch on `collections.abc.Mapping` and `Sequence`/`Set` now, with `str` and `bytes` handled first so a string cannot be exploded into a list of characters, and a counterweight test so widening the check cannot leave the repr branch dead. (#730) + +- **A per-run id inside an agent name poisoned the facet anyway.** `normalize_agent_id` exists because `agent_id` is a `LowCardinality(String)` and the primary facet on every dashboard surface, but it only caught a value that was an id *all the way through*. `agent-`, `crew_`, `task-3f9a1c2b-…` — a readable name carrying a per-run suffix — went straight past it, which is the shape frameworks actually produce and the exact one the CrewAI page already warns against. The id is stripped and the readable part kept, so `agent-` records as `agent` rather than collapsing to `main` and discarding the only meaningful token; dashed UUIDs are matched as a substring before the segment pass, or splitting on separators would break the most standard shape of all into five individually-innocent pieces. A name where nothing was stripped is returned unchanged, separators included, so this cannot quietly rename every `node_a_b` to `node a b`. (#730) + +- **CrewAI never recorded a crew blocked on a human.** `crewai.flow.runtime` emits `HumanFeedbackRequestedEvent` before it blocks and `HumanFeedbackReceivedEvent` after the answer; the adapter subscribed to neither, so the entire wait was an unexplained gap in the trace and the session's active duration absorbed it. The LangChain and LlamaIndex adapters both map their HITL surface onto the same four events and CrewAI now does too — `human_wait` + `agent_pause`, then `agent_resume` + `human_input`, in that order, because only the first pair carries the prompt and the answer and only the second feeds paused time. Two things surfaced while fixing it, each of which would have left the fix silently inert. The adapter resolved event classes against `crewai.events.event_types` alone, and the flow events are not in it — they are lazily re-exported from `crewai.events` — so the lookup returned `None`, the capability probe disabled that one hook, and nothing failed; `event_class()` now tries both namespaces, and the anti-drift test resolves through it rather than through a namespace of its own, since asserting against the narrower one is what let the gap exist. And CrewAI sets **no correlation id on either event** (`request_id` is `None` on both, `started_event_id` `None` on the received one), so pairing on it raised a `TypeError` inside the customer's event bus; the join is now `request_id` — which the enterprise async provider does set, and which can interleave — then `(flow_name, method_name)`, then the most recently opened pause, which is sound only because a console prompt blocks. Feedback for a request we never saw records the answer but deliberately withholds `agent_resume`, since closing a pause that never opened subtracts an interval that was never added. Six tests, all six failing if reverted. (#730) + +- **Corrected the LlamaIndex adapter's claim about token fidelity.** It said an integration naming its counters something unusual would show blank token columns, which reads as an exotic case. The common case is worse and was undocumented: `FunctionAgent` — the agent API LlamaIndex documents — calls `astream_chat`, and `llama-index-llms-openai` does not send `stream_options={"include_usage": True}`, so the provider never emits the usage chunk and `LLMChatEndEvent.response.raw` has no `usage` key on it. Verified by spying on the dispatcher directly against llama-index-core 0.14.23: every `LLMChatEndEvent` in a `FunctionAgent` run arrives with usage absent, so **every token count on the default agent path is null** and no instrumentation can recover a number the framework never received. Non-streaming `llm.chat`/`achat` extract usage correctly with no configuration. The one-argument user-side fix is now stated in the docstring, in the docs and used by the shipped examples: measured on the same run, `(None, None)` becomes `(148, 17)`. (#730) + +### Docs + +- **Audited every checkable doc claim against the SDK, and four were wrong.** The guides were written per-framework and the option lists were prose, so nothing caught a page describing an argument its adapter does not read — and nothing would have, because `instrument()` passes one dict to every adapter and drops unknown keys by design. A reader following those pages got no error and no effect. The CrewAI guide named `capture_content` (that adapter reads only `session_id`); the LlamaIndex guide named `session_id` and `capture_content` (it reads neither — its options are `embeddings`, `steps`, `capture_messages`, `stale_after` and `reaper_interval`, and its session comes from a scope); the Pydantic AI guide told readers to verify the install with `agent.capabilities`, which raises `AttributeError` because Pydantic AI merges the list into one `root_capability`; and the spool page printed a batch filename in a format the writer has never produced. LangGraph's options were correct. Also noted that `include_chains` applies to nested runs only — a top-level runnable is the session's root and becomes the agent span, so naming it there does nothing, which was verified rather than assumed. Three tests now hold the line: documented `instrument()` options are parsed out of each adapter's own source and compared, on both doc copies; the Pydantic AI verification snippet is pinned to `root_capability`; and the filename printed on the page is matched against one the writer actually wrote. Each was negative-controlled by reintroducing the original mistake. (#730) + +- **`sdk/python/docs/`: the guide and the code that proves it, in one tree.** Documentation and examples were two directories that had to be kept in agreement by hand, and the docs half was MDX — which renders as raw JSX tags anywhere except a Mintlify build, so on disk and on GitHub it read as broken markup. Both are now one Markdown tree, a directory per framework, each holding the guide somebody reads and the `examples/` they run: `docs//README.md` beside `docs//examples/*.py`. Five directories — `langgraph`, `crewai`, `llama_index`, `pydantic_ai` and `manual` for an agent with no framework, which also carries the three-seam recipe for any unsupported one and states why AutoGen has no adapter. Each guide covers install and supported version range, the three-line integration, the attach mechanism, a full framework-concept-to-event mapping table, a complete copy-pasteable program, agent naming, session resolution, every `instrument()` option, a real captured event payload, and pitfalls written as symptom then cause then fix. The pitfalls are the ones that actually cost time and were each hit while writing this: construct Pydantic AI agents *after* `instrument()` or they carry no capability and record nothing, with no error; `create_react_agent` aborts the graph on a raising tool unless the tool node sets `handle_tool_errors`; LlamaIndex needs one `stream_options` argument or every token count is null; and never read the spool to verify anything, because a running `failproofaid` deletes each batch within milliseconds and the read races it. (#730) + +- **Every example was executed against a live model before it shipped, and each prints its own trace.** Eleven scripts across the five directories — a supervisor delegating to two workers (38 events, 5 agents), a bare OpenAI tool-calling loop instrumented entirely by hand (14 events, no framework), and multi-tool runs for each adapter including one deliberately failing tool so the recovery path is visible. The traces quoted in the guides are captured output, not illustrations. Each ends by printing the event stream it produced, captured by tapping the writer in-process rather than reading the spool, for the reason above. `test_examples.py` and `test_docs.py` merge into one `test_docs.py` that walks the whole tree: a framework with an adapter and no directory fails, a guide linking to an example that does not exist fails, a guide or example naming SDK API that does not exist fails, and an example threading `session_id=` by hand fails — checked over the AST, so the manual guide can still *explain* the argument its whole purpose is to replace. (#730) + +- **Event pairings key on the session, never on the agent.** `_pending` correlates a start event with its end so `duration_ms` can be measured, and the key had been wrong twice in opposite directions. Bare ids let a tool call and a hook sharing an id consume each other's timestamp; adding the session fixed a real collision between two concurrent runs in one process. Adding the AGENT as well was over-tightening, and is reverted here: once a framework runs tools inside sub-agents — LangGraph and CrewAI both do — a `tool_use` opened under `planner` and closed under `worker` is the ordinary case, and an agent-scoped key makes those pairs miss entirely, silently dropping `duration_ms` for exactly the nested runs that most need it. The rule that survives both: include what makes the id unique (kind, session), exclude what can legitimately change between the two events. Applied to all four pair types. Correlation keys never leave the process, so no wire format changes — only `duration_ms`, in the colliding cases, from a fabricated or missing value to a correct one. (#730) + - **Guard the `fp-cli` credential path against drifting from the layout register.** `~/.failproofai/` is a governed layout — `src/hooks/fp-home.ts` declares it and "nothing outside this file may join a path onto the failproofai home" — and what actually keeps a reset off the CLI's session is its `user-typed` entry in `HOME_CLASSES`, since `resettablePaths()` is a *filter over* that table. Nothing checked that the two sides agreed, and `config.py` said so itself above `FPCLI_SUBDIR`: "change one, change the other; nothing checks". Confirmed by experiment: renaming `fpcliDir` to `fp-cli` in the TypeScript and leaving Python alone left 53 TS tests and 59 Python tests all passing, with the register describing a directory nothing writes and the real credential at a path it had never heard of — safe only until somebody classifies its parent. `fp-cli/tests/test_fp_home_contract.py` now reads `fp-home.ts` and pins the subdirectory name, the filename, the home directory, the `FAILPROOFAI_HOME` override, the `user-typed` classification and the deliberate absence of a class on the directory itself. It mirrors the SDK's `test_spool_contract.py`, including the parts that stop a source-reading test passing vacuously: every pattern must match exactly once, the anchors are asserted separately, and CI sets `FP_CLI_REQUIRE_CONTRACT=1` so a moved register fails instead of skipping. Verified end to end as well — a real `fp` session planted in a populated home survives a real `resettablePaths()` reset, while `audit/cache` beside it is removed. (#702) - **Three comments about the CLI's home contradicted the shipped code.** `fp-home.ts` said the move from `~/.fp/cli.json` "deliberately did NOT migrate the old file… so the upgrade costs a login rather than a credential-rewriting code path that runs once and is never tested again"; `config.py` said the legacy file is "Never read, never written, never deleted"; and `test_failproofai_home.py`'s own module docstring said it is "neither read nor deleted", 200 lines above the tests that assert it IS adopted. All three describe the behaviour before `ce2012db` added adoption: `load_config` reads the pre-move session, writes it to the new path and leaves the original so a downgrade still finds it — best-effort, and it costs no login. The tests were updated when adoption landed and the prose was not. Also corrected `fp-home.ts`'s citation of `home-classification.test.ts`, a file that has never existed; the classification guard is real and lives in `__tests__/hooks/fp-home.test.ts`. (#702) diff --git a/crates/failproofaid/src/main.rs b/crates/failproofaid/src/main.rs index 51a3a456b..b3c9207a3 100644 --- a/crates/failproofaid/src/main.rs +++ b/crates/failproofaid/src/main.rs @@ -734,6 +734,9 @@ fn collector_tasks() -> Vec { // One `Delivery` shared by both tasks, so they share an upload semaphore // and an in-flight set. Separate ones would let the watcher and a // concurrent sweep POST the same batch twice. + // Grabbed before the uploader moves into `Delivery`. The counters live on + // the `Uploader` itself so a supervised task restart never rewinds them. + let upload_metrics = uploader.metrics(); let delivery = std::sync::Arc::new(fpai_collect::Delivery::new(uploader)); let watch_delivery = delivery.clone(); @@ -750,6 +753,12 @@ fn collector_tasks() -> Vec { // daemon", where a stale file makes a stopped daemon look like a running // one whose sources all went quiet. let health = std::sync::Arc::new(fpai_collect::Health::new()); + // Before `install`, so the first snapshot already carries delivery. The + // source map alone cannot say whether anything is ARRIVING — a source's job + // ends at the spool — and the SDK's batches have no source entry at all, so + // a machine shipping only SDK events reported an empty, healthy-looking file + // whether ingest was storing every event or discarding all of them. + health.attach_delivery(upload_metrics); fpai_collect::health::install(health.clone()); let health_file = fpai_collect::health_path(&home); tasks.push(fpai_collect::TaskSpec::new("health", move |sd| { diff --git a/crates/failproofaid/tests/collector_reload_e2e.rs b/crates/failproofaid/tests/collector_reload_e2e.rs index 23cf8c8c2..017badcd1 100644 --- a/crates/failproofaid/tests/collector_reload_e2e.rs +++ b/crates/failproofaid/tests/collector_reload_e2e.rs @@ -256,22 +256,57 @@ fn disabling_collection_stops_it_and_re_enabling_starts_it_again() { // `--disconnect` used to require a restart to take effect, so a machine that // had left its organisation went on shipping. The reverse matters just as // much: re-enabling must not need one either, or the fix is half a fix. + // + // The lever is the CREDENTIAL, because that is what `--disconnect` actually + // removes (`clearIngestCredential` in cloud-enrollment-cli.ts). This used to + // toggle `collector.hooks` as a stand-in, which no longer disables anything: + // that flag gates the daemon's own hook-activity source and deliberately + // does NOT gate delivery, since the spool also carries batches the + // `failproofai-sdk` wrote from the user's own process. Toggling the real + // thing is a stronger test of the scenario this exists for, and the + // companion test below pins the behaviour that replaced the old lever. let home = unique_home("toggle"); make_home(&home, "a-stable-key"); let daemon = spawn_daemon(&home); daemon.wait_for("collector enabled", 1, Duration::from_secs(20)); + let creds = home.join("credentials.json"); + let on = std::fs::read_to_string(&creds).unwrap(); + std::fs::remove_file(&creds).unwrap(); + daemon.wait_for("no longer enabled", 1, Duration::from_secs(20)); + + std::fs::write(&creds, &on).unwrap(); + daemon.wait_for("collector enabled", 2, Duration::from_secs(20)); + let _ = std::fs::remove_dir_all(&home); +} + +#[test] +fn turning_both_capture_sources_off_leaves_delivery_running() { + // The counterweight to the change above, and the bug it came from: with + // `sessions` and `hooks` both false the daemon used to start NOTHING — no + // spool watcher, no sweeper, no log line — so every batch the SDK wrote + // sat on disk forever, with no error on either side and an unread spool + // indistinguishable from an idle one. + // + // Those two settings gate the daemon's own capture sources, and each is + // checked again where its source is registered, so leaving them off still + // starts neither. What they must not gate is delivery. + let home = unique_home("sources-off"); + make_home(&home, "a-stable-key"); let cfg = home.join("config.json"); let on = std::fs::read_to_string(&cfg).unwrap(); - // JSON now, so the edit is on the key/value pair, not a TOML line. A - // string replace that silently matches nothing writes the file back - // unchanged and the test then waits 20s for a reload that never had a - // reason to happen — which is exactly how this broke. std::fs::write(&cfg, on.replace(r#""hooks":true"#, r#""hooks":false"#)).unwrap(); - daemon.wait_for("no longer enabled", 1, Duration::from_secs(20)); - std::fs::write(&cfg, &on).unwrap(); - daemon.wait_for("collector enabled", 2, Duration::from_secs(20)); + let daemon = spawn_daemon(&home); + daemon.wait_for("collector enabled", 1, Duration::from_secs(20)); + // The watcher is the thing that ships an SDK batch; without it this daemon + // is a process that reports healthy and delivers nothing. + daemon.wait_for("spool watcher started", 1, Duration::from_secs(20)); + assert!( + !daemon.stderr().contains("hook-activity source started"), + "hooks=false must still switch the daemon's own hook source off:\n{}", + daemon.stderr() + ); let _ = std::fs::remove_dir_all(&home); } diff --git a/crates/fpai-collect/src/config.rs b/crates/fpai-collect/src/config.rs index 87c2ca2de..6acb75e80 100644 --- a/crates/fpai-collect/src/config.rs +++ b/crates/fpai-collect/src/config.rs @@ -261,11 +261,25 @@ pub struct CollectorConfig { } impl CollectorConfig { - /// True when there is a usable credential AND at least one stream enabled. - /// This is what `collector_tasks()` keys off, so an unconfigured machine - /// starts no thread and no runtime. + /// True when there is a usable credential. This is what `collector_tasks()` + /// keys off, so an unconfigured machine starts no thread and no runtime. + /// + /// Deliberately NOT `&& (sessions || hooks)`. Those two gate the daemon's + /// OWN capture sources — CLI session transcripts and hook activity — and + /// each is checked again where its source is registered, so leaving them + /// off still starts neither. What they must not gate is DELIVERY, because + /// the spool also carries batches this daemon did not produce: the + /// `failproofai-sdk` writes its own events into `custom-agents/events/`, + /// and the spool watcher is the only thing that ships them. + /// + /// While they did gate it, `collector.hooks = false` — a documented + /// privacy choice, and the only one available to somebody who wants their + /// instrumented agents shipped and nothing else — silently disabled the + /// SDK too: no task started, no line logged, and batches accumulated in + /// the spool forever. An unread spool is indistinguishable from an idle + /// one, which is the exact failure this project exists to remove. pub fn is_enabled(&self) -> bool { - self.ingest.is_some() && (self.settings.sessions || self.settings.hooks) + self.ingest.is_some() } } diff --git a/crates/fpai-collect/src/health.rs b/crates/fpai-collect/src/health.rs index 281299333..05288a6ec 100644 --- a/crates/fpai-collect/src/health.rs +++ b/crates/fpai-collect/src/health.rs @@ -55,6 +55,37 @@ pub struct SourceHealth { pub errors: u64, } +/// What delivery reports about itself, across every source AND the SDK spool. +/// +/// The source map above cannot answer "is anything actually arriving", because +/// a source's job ends when it writes a batch to the spool. Everything after +/// that — the POST, the server's verdict, the parking of what would not go — is +/// invisible to it, and the SDK's own batches have no source entry at all: the +/// `failproofai-sdk` writes them straight into the spool from the user's +/// process, so a machine shipping nothing but SDK events reports a perfectly +/// healthy, entirely empty `sources` map. +/// +/// `skipped` is the one worth staring at. Ingest answers `200` with +/// `{"accepted":N,"skipped":M}` and the daemon deletes the batch either way, so +/// a systematically malformed field — an `environment` containing a comma, say +/// — discards every event on the machine while every layer reports success. +/// Before this, the only trace was a line in the daemon's log. +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub struct DeliveryHealth { + /// Events the server said it stored, since this daemon started. + pub accepted: u64, + /// Events the server refused as malformed. Non-zero means data loss that + /// nothing else on the machine will tell you about. + pub skipped: u64, + /// Batches answered `200` while storing NONE of their events — the shape a + /// systematic problem takes, as opposed to one bad line. + pub batches_fully_skipped: u64, + /// Unix seconds of the last upload the server accepted. Zero means not one + /// has succeeded since startup, which on a machine that is producing events + /// is the loudest thing in this file. + pub last_ok_ts: u64, +} + /// The whole record, as written. #[derive(Debug, Default, Deserialize, Serialize)] pub struct HealthFile { @@ -62,6 +93,11 @@ pub struct HealthFile { /// current one — a daemon that died leaves its last record behind. pub ts: u64, pub sources: BTreeMap, + /// Absent when this daemon has no uploader, which is every daemon with no + /// credential. Skipped rather than zeroed: all-zero counters and "delivery + /// is not configured" are different facts and must not render the same. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub delivery: Option, } /// Shared, cheap-to-update health state. @@ -73,6 +109,9 @@ pub struct HealthFile { pub struct Health { sources: Mutex>, writes: AtomicU64, + /// Set once at startup when there is an uploader. Read, never written, so + /// the counters stay owned by the `Uploader` that survives a task restart. + delivery: Mutex>>, } impl Health { @@ -109,12 +148,28 @@ impl Health { entry.last_error = Some(truncate(error, MAX_ERROR_LEN)); } + /// Report delivery counters alongside the sources. Called once, at startup. + pub fn attach_delivery(&self, metrics: std::sync::Arc) { + if let Ok(mut slot) = self.delivery.lock() { + *slot = Some(metrics); + } + } + /// Snapshot for writing. pub fn snapshot(&self) -> HealthFile { let sources = self.sources.lock().map(|m| m.clone()).unwrap_or_default(); + let delivery = self.delivery.lock().ok().and_then(|slot| { + slot.as_ref().map(|m| DeliveryHealth { + accepted: m.accepted_total.load(Ordering::Relaxed), + skipped: m.skipped_total.load(Ordering::Relaxed), + batches_fully_skipped: m.batches_fully_skipped.load(Ordering::Relaxed), + last_ok_ts: m.last_ok_ts.load(Ordering::Relaxed), + }) + }); HealthFile { ts: now_secs(), sources, + delivery, } } diff --git a/crates/fpai-collect/src/lib.rs b/crates/fpai-collect/src/lib.rs index f6d50a651..4ae27186e 100644 --- a/crates/fpai-collect/src/lib.rs +++ b/crates/fpai-collect/src/lib.rs @@ -31,7 +31,7 @@ pub use config::{ }; pub use delivery::Delivery; pub use extra_paths::{ExtraPath, Resolved as ResolvedExtraPaths}; -pub use health::{Health, HealthFile, SourceHealth, health_path}; +pub use health::{DeliveryHealth, Health, HealthFile, SourceHealth, health_path}; pub use spool::SpoolWriter; pub use supervisor::{ CollectorHandle, DEFAULT_FLUSH_BUDGET, Shutdown, SupervisorMetrics, TaskError, TaskSpec, diff --git a/crates/fpai-collect/tests/config.rs b/crates/fpai-collect/tests/config.rs index b44497d63..984d4826e 100644 --- a/crates/fpai-collect/tests/config.rs +++ b/crates/fpai-collect/tests/config.rs @@ -79,6 +79,39 @@ fn a_key_alone_does_not_enable_session_collection() { fs::remove_dir_all(&home).ok(); } +#[test] +fn a_key_with_both_sources_off_still_delivers_the_sdk_spool() { + // `collector.hooks = false` is a documented privacy choice. It must switch + // off the daemon's own hook-activity source and NOTHING else: the spool it + // watches also holds batches written by `failproofai-sdk` from the user's + // own instrumented agents, and the watcher is the only thing that ships + // them. While this returned false for that config the daemon started no + // task and logged no line, and those batches piled up forever. + let home = tmp_home("bothoff"); + config::write_ingest( + &home, + &Ingest { + url: DEFAULT_INGEST_URL.into(), + key: "k".into(), + }, + ) + .unwrap(); + fs::write( + home.join("config.json"), + r#"{"collector":{"sessions":false,"hooks":false}}"#, + ) + .unwrap(); + + let cfg = without_env_overrides(|| config::load(&home).unwrap()); + assert!(!cfg.settings.sessions); + assert!(!cfg.settings.hooks); + assert!( + cfg.is_enabled(), + "delivery must run for the SDK spool even with both capture sources off" + ); + fs::remove_dir_all(&home).ok(); +} + #[test] fn the_credential_file_is_written_owner_only() { #[cfg(unix)] diff --git a/crates/fpai-collect/tests/health_delivery.rs b/crates/fpai-collect/tests/health_delivery.rs new file mode 100644 index 000000000..2873686e8 --- /dev/null +++ b/crates/fpai-collect/tests/health_delivery.rs @@ -0,0 +1,91 @@ +//! The health file must be able to say whether anything is ARRIVING. +//! +//! Its source map cannot. A source's job ends when it writes a batch into the +//! spool — the POST, the server's verdict and the parking of what would not go +//! are all after that — and the SDK's batches have no source entry at all, +//! because `failproofai-sdk` writes them into the spool from the user's own +//! process. So a machine shipping nothing but SDK events produced a file with +//! an empty, perfectly healthy `sources` map whether ingest was storing every +//! event or discarding all of them. + +use std::sync::Arc; +use std::sync::atomic::Ordering; + +use fpai_collect::{Health, HealthFile, UploadMetrics}; + +#[test] +fn delivery_is_absent_until_an_uploader_attaches() { + // All-zero counters and "this daemon has no credential, so nothing is being + // delivered at all" are different facts. Rendering them the same would make + // an unconfigured machine look like a broken one, and vice versa. + let health = Health::new(); + let json = serde_json::to_string(&health.snapshot()).unwrap(); + assert!( + !json.contains("delivery"), + "an unconfigured daemon must omit the section, not zero it: {json}" + ); +} + +#[test] +fn attached_delivery_counters_reach_the_snapshot() { + let health = Health::new(); + let metrics = Arc::new(UploadMetrics::default()); + health.attach_delivery(metrics.clone()); + + metrics.accepted_total.fetch_add(120, Ordering::Relaxed); + metrics.skipped_total.fetch_add(7, Ordering::Relaxed); + metrics + .batches_fully_skipped + .fetch_add(1, Ordering::Relaxed); + metrics.last_ok_ts.store(1_787_216_518, Ordering::Relaxed); + + let snap = health.snapshot(); + let delivery = snap + .delivery + .expect("delivery must be reported once attached"); + assert_eq!(delivery.accepted, 120); + assert_eq!( + delivery.skipped, 7, + "skipped is the counter that means data loss" + ); + assert_eq!(delivery.batches_fully_skipped, 1); + assert_eq!(delivery.last_ok_ts, 1_787_216_518); +} + +#[test] +fn the_counters_are_read_live_rather_than_copied_at_attach() { + // The `Uploader` owns them and outlives any supervised task restart, so the + // health writer must read through to it. Snapshotting the values at attach + // time would freeze the file at "nothing has happened yet" forever — which + // reads exactly like a healthy idle machine. + let health = Health::new(); + let metrics = Arc::new(UploadMetrics::default()); + health.attach_delivery(metrics.clone()); + assert_eq!(health.snapshot().delivery.unwrap().accepted, 0); + + metrics.accepted_total.fetch_add(5, Ordering::Relaxed); + assert_eq!(health.snapshot().delivery.unwrap().accepted, 5); +} + +#[test] +fn a_written_file_round_trips_through_the_published_type() { + // `HealthFile` is what a reader outside this crate deserializes; an added + // field that only serializes one way would be invisible to them. + let dir = std::env::temp_dir().join(format!("fpai-health-{}", std::process::id())); + let path = dir.join("collector-health.json"); + let health = Health::new(); + let metrics = Arc::new(UploadMetrics::default()); + metrics.skipped_total.fetch_add(3, Ordering::Relaxed); + health.attach_delivery(metrics); + health.write(&path).unwrap(); + + let parsed: HealthFile = + serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap(); + assert_eq!(parsed.delivery.expect("round trip").skipped, 3); + + // An older file, written before this section existed, must still parse. + let legacy: HealthFile = serde_json::from_str(r#"{"ts":1,"sources":{}}"#).unwrap(); + assert!(legacy.delivery.is_none()); + + std::fs::remove_dir_all(&dir).ok(); +} diff --git a/docs/docs.json b/docs/docs.json index 9c61d2eb5..e5fe7b46e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -80,7 +80,20 @@ "start/first-audit", "start/first-policy", "start/setup", - "start/concepts" + "start/concepts", + "start/integrations", + { + "group": "Plug in your agents", + "expanded": false, + "pages": [ + "start/integrations/how-it-works", + "start/integrations/custom-agents", + "start/integrations/langchain", + "start/integrations/crewai", + "start/integrations/llamaindex", + "start/integrations/pydantic-ai" + ] + } ] } ] diff --git a/docs/reference/overview.mdx b/docs/reference/overview.mdx index 7488d280c..8dc02cd88 100644 --- a/docs/reference/overview.mdx +++ b/docs/reference/overview.mdx @@ -10,8 +10,11 @@ Choose the integration closest to where your agent already runs. Install hooks for supported coding and autonomous agent CLIs. - - Instrument traces to find failures in custom agents, then contact us to add prevention to your runtime. + + Instrument LangGraph, CrewAI, LlamaIndex, Pydantic AI, or a custom agent. + + + Configuration, the event catalog, correlation rules, and delivery. Review local projects, sessions, policy activity, and offline audits. diff --git a/docs/reference/python-sdk.mdx b/docs/reference/python-sdk.mdx index 73cd0ffbf..01ed72e8d 100644 --- a/docs/reference/python-sdk.mdx +++ b/docs/reference/python-sdk.mdx @@ -1,34 +1,35 @@ --- -title: "Custom agents" -description: "Instrument traces from custom agents so Failproof AI can reconstruct runs and find failures." +title: "Python SDK reference" +description: "Configuration, the event catalog, correlation rules, and delivery for failproofai-sdk." icon: "python" --- -Instrument traces from a custom agent with `failproofai-sdk` so Failproof AI can reconstruct each run, audit its behavior, and find evidence-backed failures. The SDK writes structured events for the Failproof daemon to deliver to Cloud. It requires Python 3.10 or newer. +Reference material for `failproofai-sdk`. To connect an agent for the first time, start with the integration guides instead. -Tracing makes custom agents observable and auditable. Preventing an unsafe action before it executes also requires an enforcement hook in your runtime. + + + LangGraph, CrewAI, LlamaIndex, Pydantic AI, and custom agents. + + + Scopes, event methods, threads, and instrumenting a framework without an adapter. + + + +The SDK writes structured events for the Failproof daemon to deliver to Cloud. It requires Python 3.10 or newer and has no runtime dependencies. + +Tracing makes agents observable and auditable. Preventing an unsafe action before it executes also requires an enforcement hook in your runtime. To enforce policies in a custom agent setup, [contact Failproof AI](mailto:support@befailproof.ai). We will help map your runtime's model, tool, and lifecycle boundaries to policy hooks. -
- -
- -## Install `failproofai-sdk` - -The SDK is currently distributed as a private wheel. Ask your Failproof AI contact for the current version and download access. +## Install ```bash -VERSION= -pip install "./failproofai_sdk-${VERSION}-py3-none-any.whl" -python -c "import failproofai; print(failproofai.__version__)" +pip install failproofai-sdk ``` -With `uv`, download the wheel first and run `uv add ./failproofai_sdk-${VERSION}-py3-none-any.whl`. Pin the wheel in a private artifact repository or dependency lock. - -The package is installed as `failproofai-sdk` and imported in Python as `failproofai`. +The package is installed as `failproofai-sdk` and imported in Python as `failproofai_sdk`. Framework extras such as `failproofai-sdk[langgraph]` install the framework itself; the adapters always ship in the base wheel. ## Connect the Failproof daemon @@ -51,73 +52,12 @@ The package is installed as `failproofai-sdk` and imported in Python as `failpro -## Instrument a complete run - -Call `configure()` once during process startup. Every event call is keyword-only and requires a stable `session_id` and `agent_id`. +## Configuration ```python -import traceback -import uuid - -import failproofai - -failproofai.configure(environment="production") +import failproofai_sdk -session_id = uuid.uuid4().hex -agent_id = "checkout-agent" - -failproofai.event.agent_start( - session_id=session_id, - agent_id=agent_id, - goal="Resolve a failed checkout", -) - -try: - tool_call_id = uuid.uuid4().hex - failproofai.event.tool_use( - session_id=session_id, - agent_id=agent_id, - tool_name="lookup_order", - tool_call_id=tool_call_id, - input={"order_id": "ord_8421"}, - ) - result = {"status": "payment_failed"} - failproofai.event.tool_result( - session_id=session_id, - agent_id=agent_id, - tool_name="lookup_order", - tool_call_id=tool_call_id, - output=result, - ) -except Exception as exc: - failproofai.event.error( - session_id=session_id, - agent_id=agent_id, - error_type=type(exc).__name__, - message=str(exc), - traceback=traceback.format_exc(), - ) - failproofai.event.agent_end( - session_id=session_id, - agent_id=agent_id, - outcome="failed", - ) - raise -else: - failproofai.event.agent_end( - session_id=session_id, - agent_id=agent_id, - outcome="success", - summary="Escalated the failed payment", - ) -``` - -Emit `agent_start` once per actor. For sub-agents, reuse the parent's `session_id`, give each actor a distinct `agent_id`, and set `parent_id` to the parent **agent ID**, not the session ID. - -## Configuration reference - -```python -failproofai.configure( +failproofai_sdk.configure( base_dir=None, flush_interval=0.5, environment="production", @@ -128,25 +68,58 @@ failproofai.configure( | --- | --- | | `base_dir` | Explicit spool root. Takes precedence over all environment variables. | | `flush_interval` | Seconds between background writes from memory to JSONL. Default: `0.5`. | -| `environment` | Deployment label on every event. Defaults to `dev`. | +| `environment` | Deployment label on every event. Defaults to `dev`. Must not contain a comma — ingest splits this field on commas to build its filter facets and skips any event whose label has one, so `configure()` raises rather than let a run vanish. Use `prod-eu`, not `prod,eu`. | | `FAILPROOFAI_HOME` | Changes the Failproof AI root that contains the `custom-agents` spool. | +| `FAILPROOFAI_SDK_STRICT` | Set to `1` to re-raise instrumentation errors instead of logging them. | -The SDK writes to the explicit `base_dir` when set. Otherwise, it uses the Failproof daemon's `custom-agents` spool under `FAILPROOFAI_HOME` or `~/.failproofai`. +The SDK writes to the explicit `base_dir` when set. Otherwise it uses the Failproof daemon's `custom-agents` spool under `FAILPROOFAI_HOME` or `~/.failproofai`. -The SDK queues calls in memory and writes batches on a background thread. It also attempts a final flush through Python's `atexit` handling. For short-lived workers, allow normal interpreter shutdown; hard process termination can lose events still in memory. +The SDK queues calls in memory and writes batches on a background thread, with a final flush through Python's `atexit` handling. For short-lived workers, allow normal interpreter shutdown; hard process termination can lose events still in memory. + + + `SIGTERM` is one of those hard terminations, and it is the one you will actually meet: every rolling deploy, every `docker stop`, every Kubernetes eviction. CPython installs no handler for it, so the process ends where it stands and the `atexit` flush never runs. What is in flight at shutdown is disproportionately `agent_end`, so runs stay open and never close. + + If your process can receive `SIGTERM`, handle it. The SDK will not install a handler in your process on your behalf. + + ```python + import signal, sys, failproofai_sdk + + def _flush_and_exit(signum, frame): + failproofai_sdk._writer.flush_now() + sys.exit(128 + signum) + + signal.signal(signal.SIGTERM, _flush_and_exit) + ``` + + `sys.exit` rather than `os._exit`: it unwinds, so an open `agent()` scope still emits its `agent_end` before the flush. That scope closes `outcome="failed"` — an evicted run did not finish, which is the thing worth being able to see. `SIGKILL` and a container OOM cannot be handled by anything. + + +## Identity + +`session_id` and `agent_id` are optional on every event method. Omitted, they resolve from the enclosing scope: + +```python +with failproofai_sdk.session(): + with failproofai_sdk.agent("planner"): + failproofai_sdk.event.tool_use(tool_name="search", tool_call_id="c1") +``` + +Passing them explicitly still works and takes precedence. With nothing bound and nothing passed, the call raises a `TypeError` naming the fix rather than emitting an event with no session, which ingest would skip while answering `200`. + +Scopes bind identity on context variables. Those propagate into asyncio tasks automatically but not into new threads — wrap a worker in `failproofai_sdk.propagate()`. ## Event catalog All methods return `None`. Fields left as `None` are omitted rather than written as JSON `null`. -| Method | Required fields beyond identity | Optional fields | +| Method | Required fields | Optional fields | | --- | --- | --- | | `agent_start` | — | `goal`, `parent_id` | | `agent_end` | — | `outcome`, `summary` | | `agent_pause` | `pause_id` | `reason`, `user_id` | | `agent_resume` | `pause_id` | `reason`, `user_id` | -| `model_request` | — | `model`, `messages`, `system`, `tools` | -| `model_response` | — | `model`, `stop_reason`, `input_tokens`, `output_tokens`, `content`, `role` | +| `model_request` | — | `model`, `messages`, `system`, `tools`, `request_id` | +| `model_response` | — | `model`, `stop_reason`, `input_tokens`, `output_tokens`, `content`, `role`, `request_id`, `duration_ms` | | `tool_use` | `tool_name`, `tool_call_id` | `input` | | `tool_result` | `tool_name`, `tool_call_id` | `output`, `error` | | `hook_triggered` | `hook_name`, `hook_id` | `trigger_event`, `input` | @@ -159,11 +132,13 @@ All methods return `None`. Fields left as `None` are omitted rather than written Use `outcome="failed"`, `"error"`, `"timeout"`, or `"rejected"` when a completion should count as a failure. Other values, including `"failure"`, are not classified as failures by the current backend. -## Correlation and duration rules +## Correlation and duration - Reuse the same `tool_call_id`, `hook_id`, `pause_id`, or `input_id` for the matching completion event. -- The SDK computes `duration_ms` for `tool_result`, `hook_completed`, `agent_resume`, and `human_input`. Passing it yourself to those methods raises `ValueError`. -- Tool and hook IDs share one process-wide pending map. Make them globally unique across concurrent sessions and across both namespaces; provider IDs or UUIDs are safest. +- The SDK computes `duration_ms` for `tool_result`, `hook_completed`, `agent_resume`, and `human_input`. Passing it to those methods raises `ValueError`. +- `duration_ms` **is** accepted on `model_response`, because only the caller knows the real provider latency. It must be an integer; a float is stored as null. +- Correlation keys are scoped by kind and session, so a tool call and a hook may safely share an id, and two concurrent sessions may reuse the same ids without colliding. They are not scoped by agent: a pair opened under one agent and closed under another still correlates, which is the ordinary case in multi-agent frameworks. +- `request_id` pairs `model_request` with `model_response`. Without it, model events pair in order per agent, so concurrent calls mispair. - A pair split across processes still correlates downstream, but the SDK cannot compute its in-process duration. - The pending map holds at most 10,000 starts and evicts the oldest entry when full. @@ -171,7 +146,11 @@ Use `outcome="failed"`, `"error"`, `"timeout"`, or `"rejected"` when a completio Every event accepts extra keyword fields. Use JSON-compatible values when downstream queries need structure. Unsupported leaves such as UUIDs, datetimes, decimals, sets, bytes, and model objects are stringified by the writer. -Reserved custom names are `timestamp`, `session_id`, `agent_id`, `type`, and `environment`. Optional-field typos are accepted as new custom fields, so review emitted JSON when a standard field does not appear in Cloud. +Reserved names are `timestamp`, `session_id`, `agent_id`, `type`, and `environment`. + + + Extra fields are merged last, so one named like a declared field, such as `model`, `tool_name`, or `outcome`, would overwrite it and change a stored column. Namespace your own fields; the framework adapters use an `fw_` prefix. Optional-field typos are accepted as new custom fields, so review emitted JSON when a standard field does not appear in Cloud. + ## Deliver and verify @@ -191,6 +170,10 @@ Reserved custom names are `timestamp`, `session_id`, `agent_id`, `type`, and `en If Cloud is empty, inspect `$FAILPROOFAI_HOME/custom-agents/events`, otherwise `~/.failproofai/custom-agents/events`. JSONL files prove SDK emission; a growing spool points to daemon configuration or delivery, while an empty spool points to instrumentation or process lifetime. + + Inspect the spool only when the daemon is stopped. While it runs, it collects and deletes each batch within milliseconds, so a directory listing races the collector and shows far fewer events than were emitted. + + ## Prevent failures in a custom runtime Use audit findings and linked traces to define the unsafe action, required evidence, and intended response. A custom enforcement integration must expose the action before execution, pass its structured input to the policy engine, and apply the resulting allow, instruct, or deny decision. diff --git a/docs/start/integrations.mdx b/docs/start/integrations.mdx new file mode 100644 index 000000000..191874adf --- /dev/null +++ b/docs/start/integrations.mdx @@ -0,0 +1,252 @@ +--- +title: "Instrument your agent" +sidebarTitle: "Frameworks" +description: "Connect any supported agent framework to Failproof AI with one call." +icon: "plug" +--- + +Your agent already produces everything worth recording — model calls, tool calls, node boundaries, human waits, failures. The framework throws it away. The SDK keeps it. + + + + Graphs, nodes, tools, retrievers, models, `interrupt()` pauses. + + + Crews, flows, agents by role, tools, memory, human feedback. + + + Workflows, steps, function agents, retrievers. + + + Typed agents, capabilities, tools, retries. + + + No framework, or one not listed here. + + + The data model, the ids, and how events reach Cloud. + + + +## Three lines, whichever framework + + +```bash LangGraph +pip install 'failproofai-sdk[langgraph]' +``` + +```bash CrewAI +pip install 'failproofai-sdk[crewai]' +``` + +```bash LlamaIndex +pip install 'failproofai-sdk[llamaindex]' +``` + +```bash Pydantic AI +pip install 'failproofai-sdk[pydantic-ai]' +``` + +```bash No framework +pip install failproofai-sdk +``` + + +```python +import failproofai_sdk + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() # detects the frameworks you imported + +with failproofai_sdk.session(): + ... # your existing agent call, unchanged +``` + +No decorators on your functions, no callback passed to your calls, no ids threaded through your code. Only the call inside the session differs: + + + + ```python + graph.invoke({"messages": [HumanMessage("...")]}) + ``` + + + ```python + Crew(agents=[analyst, writer], tasks=[gather, summarise]).kickoff() + ``` + + + ```python + await agent.run("...") # under `async with failproofai_sdk.session():` + ``` + + + ```python + agent.run_sync("...") + ``` + + + ```python + with failproofai_sdk.agent("planner"): + with failproofai_sdk.tool_call("search", input={"q": q}) as t: + t.output = search(q) + ``` + + + +The extra installs the **framework**. Every adapter ships in the base wheel, so a project that already has its framework needs no extra at all. + +## What you get back + +Every recording has the same shape: a span opens, work nests inside it, and each opening event gets a closing one. + +```mermaid +flowchart LR + S(["agent_start"]) --> H["hook_triggered"] + H --> M["model_request
model_response"] + H --> T["tool_use
tool_result"] + M --> C["hook_completed"] + T --> C + C --> E(["agent_end"]) +``` + +The **pair** is the unit. Each closing event carries a duration the SDK measures from its opening one. + +Below is one real run per framework — captured from the examples that ship with the SDK, model name normalised. Note how much comes back from a single call. + + + + ```text 14 events + 1 +0.000s agent_start LangGraph + 2 +0.001s hook_triggered agent + 3 +0.002s model_request gpt-4o-mini + 4 +3.023s model_response gpt-4o-mini · 21 out-tok + 5 +3.024s hook_completed agent + 6 +3.024s hook_triggered tools + 7 +3.025s tool_use word_count + 8 +3.025s tool_result word_count · ok + 9 +3.025s hook_completed tools + 10 +3.026s hook_triggered agent + 11 +3.027s model_request gpt-4o-mini + 12 +5.717s model_response gpt-4o-mini · 5 out-tok + 13 +5.720s hook_completed agent + 14 +5.721s agent_end LangGraph · success + ``` + + Nodes become hook pairs, so you get per-node latency without them crowding the agent list. + + + + ```text 10 events + 1 +0.000s agent_start crew + 2 +0.050s agent_start analyst · under crew + 3 +0.057s model_request gpt-4o-mini + 4 +3.475s model_response gpt-4o-mini · 19 out-tok + 5 +3.478s tool_use lookup_metric + 6 +3.478s tool_result lookup_metric · ok + 7 +3.486s model_request gpt-4o-mini + 8 +5.694s model_response gpt-4o-mini · 9 out-tok + 9 +5.727s agent_end analyst · success + 10 +5.739s agent_end crew · success + ``` + + Each agent's `role` becomes its span name, so latency and token spend break down per role. + + + + ```text 26 events + 1 +0.000s agent_start Agent + 2 +0.001s hook_triggered init_run + 4 +0.501s hook_triggered setup_agent + 6 +0.503s hook_triggered run_agent_step + 7 +0.505s model_request gpt-4o-mini + 8 +3.083s model_response gpt-4o-mini · 18 out-tok + 10 +3.197s hook_triggered parse_agent_output + 12 +3.355s hook_triggered call_tool + 13 +3.355s tool_use city_population + 14 +3.355s tool_result city_population · ok + 16 +3.356s hook_triggered aggregate_tool_results + ... second iteration + 26 +7.038s agent_end Agent · success + ``` + + The agent loop itself is visible, not only its model calls. + + + + ```text 8 events + 1 +0.000s agent_start agent + 2 +0.001s model_request gpt-4o-mini + 3 +4.413s model_response gpt-4o-mini · 17 out-tok + 4 +4.415s tool_use population + 5 +4.415s tool_result population · ok + 6 +4.416s model_request gpt-4o-mini + 7 +8.118s model_response gpt-4o-mini · 6 out-tok + 8 +8.119s agent_end agent · success + ``` + + No hook pairs: Pydantic AI has no node or step boundary to bracket. + + + + ```text 6 events + 1 +0.000s agent_start main + 2 +0.000s tool_use population + 3 +0.000s tool_result population · ok + 4 +0.000s model_request gpt-4o-mini + 5 +0.000s model_response gpt-4o-mini · 3 out-tok + 6 +0.000s agent_end main · success + ``` + + You emit these yourself. Same event types, same fidelity — it costs you the call sites. + + + +## The event types + +| Group | Events | +| --- | --- | +| Agents | `agent_start`, `agent_end`, `agent_pause`, `agent_resume` | +| Models | `model_request`, `model_response` | +| Tools | `tool_use`, `tool_result` | +| Hooks | `hook_triggered`, `hook_completed` | +| Humans | `human_wait`, `human_input`, `human_pause`, `human_interrupt` | +| Failures | `error` | + +Which framework records what, measured from the runs above: + +| Event | LangGraph | CrewAI | LlamaIndex | Pydantic AI | Custom | +| --- | :--: | :--: | :--: | :--: | :--: | +| Agent start and end | Yes | Yes | Yes | Yes | You | +| Model request and response | Yes | Yes | Yes | Yes | You | +| Tool use and result | Yes | Yes | Yes | Yes | You | +| Hook triggered and completed | Node | Task | Step | — | You | +| Error | Yes | Yes | Yes | Yes | Automatic | +| Human wait and input | Yes | Yes | Yes | — | You | +| Agent pause and resume | Yes | Yes | Yes | — | You | + +A dash means the framework has no such concept. `human_pause` and `human_interrupt` describe a *person* acting on the agent, which no framework signals — emit those yourself. + +## Check it arrived + +Run one instrumented session, then open **Observe → Sessions** and select your environment. The run appears as a reconstructed trace. + +If nothing arrives, confirm the machine is connected with `failproofai config --status`. See [Set up capture](/start/setup). + + + Do not check the spool directory to confirm delivery. The Failproof daemon collects and deletes each batch within milliseconds, so reading it races the collector and shows far fewer events than were emitted. + + +## Next + + + + Pairs, ids, session lifecycle, and delivery. + + + Follow causality through a session instead of disconnected logs. + + + Audit the sessions you just captured. + + diff --git a/docs/start/integrations/crewai.mdx b/docs/start/integrations/crewai.mdx new file mode 100644 index 000000000..3a1d99bd5 --- /dev/null +++ b/docs/start/integrations/crewai.mdx @@ -0,0 +1,215 @@ +--- +title: "CrewAI" +sidebarTitle: "CrewAI" +description: "Instrument crews, flows, agents by role, tools, memory, and human feedback." +icon: "users" +--- + +## Install + +```bash +pip install 'failproofai-sdk[crewai]' +``` + +Supported: `crewai` 1.13 to 2.0. 1.13 is the release that added `started_event_id` and normalized token usage, both of which the adapter relies on to pair events and report tokens. + +## Instrument + +```python +import failproofai_sdk + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +with failproofai_sdk.session(): + Crew(agents=[analyst, writer], tasks=[gather, summarise]).kickoff() +``` + +`instrument()` registers a listener on CrewAI's module-level event bus and subscribes one handler per event class. Nothing about your crew, agents, tasks, or tools changes. + +## What gets recorded + +| CrewAI | Failproof event | +| --- | --- | +| Crew kickoff | `agent_start`, `agent_end` | +| `Agent.kickoff()` (a lite agent, no crew) | `agent_start`, `agent_end`, with `agent_id` from the role | +| Flow start and finish | `agent_start`, `agent_end`; a crew kicked off inside a flow method nests under it | +| Agent execution | Nested `agent_start`, `agent_end`, with `agent_id` from the role. Under a hierarchical process a delegated coworker nests under the manager, not beside it | +| Task | Nothing; recorded as a link so children resolve to the crew | +| Flow method, guardrail | `hook_triggered`, `hook_completed` | +| Tool usage | `tool_use`, `tool_result` | +| Memory and knowledge operations | `tool_use`, `tool_result`, named for the surface hit | +| LLM call | `model_request`, `model_response`, with token usage | +| Stream chunk | Folded into the response as chunk count and time to first token | +| Human feedback requested | `human_wait`, `agent_pause` | +| Human feedback received | `agent_resume`, `human_input` | +| Agent execution error | `error`, then `agent_end` with outcome `failed` | + +A task emits nothing on purpose. A CrewAI task is a subset of the agent execution that runs it, so emitting both would double every row and render them as siblings. The task id and name ride along on the agent's own events instead. + +Memory and knowledge operations are recorded as tools, named for the surface they hit, so they appear next to your real tools where you can compare their latency. + +The nesting is what makes the trace readable on a hierarchical crew: the manager sits under the crew, and each coworker it delegates to sits under the manager. CrewAI parents a delegated execution on the `delegate_work_to_coworker` tool event rather than on the manager directly, so the adapter follows that link — otherwise every agent comes out a sibling of every other and the delegation structure is lost. + +## Example + +```python +import failproofai_sdk +from crewai import Agent, Crew, Process, Task +from crewai.tools import tool + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +MODEL = "openai/gpt-4o-mini" +METRICS = {"revenue": "$4.2M ARR, up 12% QoQ", "churn": "3.1% monthly, up from 2.4%"} + + +@tool("lookup_metric") +def lookup_metric(name: str) -> str: + """Look up a business metric by name. Valid: revenue, churn.""" + return METRICS.get(name.lower().strip(), "unknown metric") + + +analyst = Agent( + role="analyst", # becomes agent_id + goal="pull the numbers that matter and state them plainly", + backstory="You read dashboards for a living.", + tools=[lookup_metric], + llm=MODEL, +) +writer = Agent( + role="writer", + goal="turn numbers into three lines an exec will read", + backstory="You write board updates. You never pad.", + llm=MODEL, +) + +gather = Task( + description="Look up 'revenue' and 'churn' with the tool.", + expected_output="Two lines, one metric each.", + agent=analyst, +) +summarise = Task( + description="Using the metrics above, write a three-line exec summary.", + expected_output="Exactly three lines.", + agent=writer, + context=[gather], +) + +with failproofai_sdk.session(): + result = Crew( + agents=[analyst, writer], + tasks=[gather, summarise], + process=Process.sequential, + ).kickoff() +``` + +The handoff is visible in the trace: the `analyst` span closes, the `writer` span opens, and both sit inside one `crew` span. + +## Name your spans + +`agent_id` comes from `Agent(role=...)`, which is what makes it a readable dashboard facet. + +```python +Agent(role="analyst", ...) # agent_id = "analyst" +Agent(role="analyst-7f3a2b", ...) # one facet entry per run +``` + +`agent_id` is a low-cardinality column. A role containing a run id or timestamp degrades it for every query anyone runs. If a role looks like an id, the adapter refuses it and puts the real value in a payload field instead. + +## Control the session + +Resolved in this order, first match winning: + +1. `instrument("crewai", session_id=...)` +2. The enclosing `failproofai_sdk.session()` scope +3. A generated `uuid4().hex`, once per crew or flow + +Wrap the kickoff to control it per run: + +```python +with failproofai_sdk.session(f"support-{ticket_id}"): + Crew(agents=[...], tasks=[...]).kickoff() +``` + +## Options + +```python +failproofai_sdk.instrument( + "crewai", + session_id=None, # pin every run to one session id +) +``` + +`session_id` is the only option this adapter reads. Prompts and completions are always recorded, truncated to the payload budget. + +## Human in the loop + +CrewAI has **two** human-in-the-loop surfaces, and both are recorded as the same four events. + +`@human_feedback` on a flow method goes through CrewAI's event bus: the runtime emits an event before it blocks on a person and another after the answer. + +`Task(human_input=True)` does not. It calls `input()` inside CrewAI's own input provider and emits no event of any kind, so the adapter wraps that provider directly — without it the entire human wait was invisible and billed as active agent time. + +Either way you get: + +```text +human_wait the prompt and its options +agent_pause starts the paused-time clock +agent_resume stops it +human_input the answer, with the wait measured +``` + +`agent_pause` to `agent_resume` is the only pair that feeds paused time. Without it, a ten-minute human wait is billed as ten minutes of active agent time. + + + CrewAI sets no correlation id on either human-feedback event, so the adapter pairs them on the flow and method name, falling back to the most recently opened pause. That is sound because a console prompt blocks. If you build a concurrent feedback provider, set `request_id` on both events. + + + + Because the `Task(human_input=True)` path is a wrapper around CrewAI's input provider rather than an event subscription, it is restored on `uninstrument()` and re-raises whatever `input()` raises, `KeyboardInterrupt` included, unchanged. + + +## Common problems + + + + A `role` contains a UUID, timestamp, or per-run suffix. Use a stable human role and put the run-specific id in the task description instead. + + + + The event bus is asynchronous, and `kickoff()` returns before the last handlers run. Drain it first: + + ```python + from crewai.events.event_bus import crewai_event_bus + + crew.kickoff() + crewai_event_bus.flush(timeout=30) + ``` + + This is a property of CrewAI, not of the SDK. + + + + `agent_end` force-closes open pauses but not tools or models, so a run that dies inside a tool call leaves that span open. Normal teardown closes whatever is still open and marks it incomplete. Only a `SIGKILL` leaves it hanging, because nothing can run. + + + + Check in this order: `instrument()` ran before `kickoff()`; there is a `with failproofai_sdk.session():` around it; `crewai` is 1.13 or newer; `FAILPROOFAI_SDK_STRICT=1` set, so a degraded hook raises instead of being swallowed. + + + +## Next + + + + Pairs, ids, session lifecycle, and delivery. + + + Follow causality through the session you just captured. + + + LangGraph, LlamaIndex, Pydantic AI, and custom agents. + + diff --git a/docs/start/integrations/custom-agents.mdx b/docs/start/integrations/custom-agents.mdx new file mode 100644 index 000000000..6cbff8732 --- /dev/null +++ b/docs/start/integrations/custom-agents.mdx @@ -0,0 +1,274 @@ +--- +title: "Custom agents" +sidebarTitle: "Custom agents" +description: "Instrument an agent you wrote yourself, or a framework without an adapter." +icon: "wrench" +--- + +For an agent you wrote yourself, or a framework Failproof AI has no adapter for. There is nothing to instrument: you emit the events. + +This is the same API the four framework adapters call underneath. They are translation tables over it. + +## Install + +```bash +pip install failproofai-sdk +``` + +No extras, and no dependencies. + +## The three scopes + +```python +import failproofai_sdk + +failproofai_sdk.configure(environment="production") + +with failproofai_sdk.session(): # identity only, emits nothing + with failproofai_sdk.agent("planner"): # agent_start, agent_end + with failproofai_sdk.tool_call("search", input={"q": q}) as t: + t.output = search(q) # tool_use, tool_result +``` + +| Scope | Emits | Purpose | +| --- | --- | --- | +| `session()` | Nothing | Binds a session id, grouping one run | +| `agent()` | `agent_start`, `agent_end` | Brackets a unit of work | +| `tool_call()` | `tool_use`, `tool_result` | Brackets one tool and measures it | + +Everything inside can omit `session_id` and `agent_id`. The scopes bind identity on context variables and every event call reads it back, so you never thread ids through your functions. + +All three work under `async with` as well as `with`. + +Nesting agents builds the tree. `parent_id` and depth are computed from the stack: + +```python +with failproofai_sdk.session(): + with failproofai_sdk.agent("supervisor"): + with failproofai_sdk.agent("researcher"): # parent_id = "supervisor" + ... +``` + +## How a scope closes + +`agent()` handles exceptions for you: + +| What happened | Events | Outcome | +| --- | --- | --- | +| Nothing raised | `agent_end` | `success` | +| `Exception` | `error`, then `agent_end` | `failed` | +| `KeyboardInterrupt`, `SystemExit` | `error`, then `agent_end` | `failed` | +| `CancelledError`, `GeneratorExit` | `agent_end` only | `cancelled` | + +The error is emitted before `agent_end`, because the dashboard closes the span at `agent_end` and anything after it is attributed to nothing. A cancellation is not a failure, so cancelled runs do not pollute the errors surface. The exception is always re-raised: a scope never swallows. + +## The event methods + + +```python Agents +failproofai_sdk.event.agent_start(agent_id="planner", goal="find the cheapest flight") +failproofai_sdk.event.agent_end(agent_id="planner", outcome="success", summary="...") +failproofai_sdk.event.agent_pause(pause_id="p1", reason="awaiting approval") +failproofai_sdk.event.agent_resume(pause_id="p1") +``` + +```python Models +failproofai_sdk.event.model_request( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "..."}], + request_id="req-1", +) +failproofai_sdk.event.model_response( + model="gpt-4o-mini", + response="...", + input_tokens=139, + output_tokens=21, + request_id="req-1", + duration_ms=5202, +) +``` + +```python Tools +failproofai_sdk.event.tool_use(tool_name="search", tool_call_id="c1", input={"q": "..."}) +failproofai_sdk.event.tool_result(tool_name="search", tool_call_id="c1", output="...") +``` + +```python Hooks +failproofai_sdk.event.hook_triggered(hook_name="retrieve", hook_id="h1", trigger_event="node") +failproofai_sdk.event.hook_completed(hook_name="retrieve", hook_id="h1", outcome="success") +``` + +```python Humans +failproofai_sdk.event.human_wait(input_id="i1", prompt="Approve?", options=["yes", "no"]) +failproofai_sdk.event.human_input(input_id="i1", response="yes") +failproofai_sdk.event.human_pause(reason="operator paused the run", user_id="dana") +failproofai_sdk.event.human_interrupt(reason="operator stopped the run", at_step="step_3") +``` + +```python Failures +failproofai_sdk.event.error( + error_type="TimeoutError", + message="provider timed out after 30s", + traceback="...", +) +``` + + +`human_wait` and `human_input` mean the agent asked a person. `human_pause` and `human_interrupt` mean a person acted on the agent, such as a stop button. No framework signals the second pair, so it is always yours to emit. + +Pass `request_id` when you make concurrent model calls. Without it, requests and responses pair in order per agent, and concurrent calls mispair. + +## Example + +A tool-calling loop against the OpenAI API, with no agent framework: + +```python +import json + +import failproofai_sdk +from openai import OpenAI + +failproofai_sdk.configure(environment="production") +client = OpenAI() +MODEL = "gpt-4o-mini" + + +def turn(messages: list): + """One model call, bracketed by the pair.""" + failproofai_sdk.event.model_request(model=MODEL, messages=messages) + reply = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS) + usage = reply.usage + failproofai_sdk.event.model_response( + model=MODEL, + response=reply.choices[0].message.content or "", + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + ) + return reply.choices[0].message + + +with failproofai_sdk.session(): + with failproofai_sdk.agent("inventory", goal="price report"): + for _ in range(4): # bounded; an unbounded agent loop is its own bug + message = turn(messages) + if not message.tool_calls: + break + messages.append(message.model_dump(exclude_none=True)) + for call in message.tool_calls: + args = json.loads(call.function.arguments or "{}") + with failproofai_sdk.tool_call( + call.function.name, tool_call_id=call.id, input=args + ) as handle: + handle.output = run_tool(call.function.name, args) + messages.append({ + "role": "tool", + "tool_call_id": call.id, + "content": str(handle.output), + }) +``` + +That produces the same six event types an adapter would give you. The complete +runnable version, with the tool definitions, ships in the SDK repository under +`docs/manual/examples/`. + +## Threads and async + +Context variables propagate into asyncio tasks automatically. They do not propagate into new threads, because a thread starts with an empty context. + +```python +# asyncio: nothing to do +async with failproofai_sdk.session(): + await asyncio.gather(worker(1), worker(2)) + +# threads: wrap the callable +pool.submit(failproofai_sdk.propagate(work), x) +threading.Thread(target=failproofai_sdk.propagate(work)).start() +loop.run_in_executor(None, failproofai_sdk.propagate(work), x) +``` + +Without `propagate()`, the worker's events raise a `TypeError` naming the fix rather than landing on no session. That is deliberate: an event with no session is skipped by ingest and answered `200`, which is the silent failure the identity layer exists to prevent. + +## Instrument a framework without an adapter + +Most agent frameworks give you three seams. Map them and you have a complete trace. + + + + ```python + with failproofai_sdk.session(): + with failproofai_sdk.agent(agent_name, goal=task): + result = framework.run(task) + ``` + + + In whatever the framework calls a tool wrapper or middleware. + + ```python + with failproofai_sdk.tool_call(name, input=args) as call: + call.output = original(**args) + ``` + + + ```python + failproofai_sdk.event.model_request(model=model, messages=messages) + reply = provider.complete(...) + failproofai_sdk.event.model_response( + model=model, + response=text, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + ) + ``` + + + +If the framework has a node, step, or middleware boundary worth seeing, add a hook pair around it rather than a nested `agent()`. `agent_id` is a low-cardinality facet, and one entry per node drowns it. + + + AutoGen has no adapter for two reasons. `autogen-core` has been unmaintained since September 2025, and AG2 exposes no process-wide registration point equivalent to the other frameworks' hooks — instrumenting it means wrapping each agent at each construction site. The three seams above record the same events at the same fidelity. + + + + **Manual and automatic compose.** An adapter running inside a hand-written + scope joins that session and parents to that agent, so you get one tree rather + than two — useful when you instrument a framework yourself alongside a + supported one. + + +## Common problems + + + + An opening event has no closing one: a `model_request` with no `model_response`, or a `tool_use` with no `tool_result`. Use the scopes, which guarantee the pair even when the body raises. If you call the event methods directly, use `try` and `finally`. + + + + It is measured from the matching opening event, so it is rejected on `tool_result`, `hook_completed`, `agent_resume`, and `human_input`. It is accepted on `model_response`, because only you know the real provider latency, and it must be an integer. + + + + The thread never inherited the context. Wrap the callable in `failproofai_sdk.propagate()`. See [Threads and async](#threads-and-async). + + + + Extra fields merge last, so one named like a real field such as `model` or `outcome` would overwrite it and change a stored column. Namespace yours; the adapters use an `fw_` prefix. + + + + `agent_id` is a low-cardinality facet and you put a run id in it. Use a role or node name and put the real id in a payload field. + + + +## Next + + + + Pairs, ids, session lifecycle, and delivery. + + + Follow causality through the session you just captured. + + + LangGraph, CrewAI, LlamaIndex, and Pydantic AI. + + diff --git a/docs/start/integrations/how-it-works.mdx b/docs/start/integrations/how-it-works.mdx new file mode 100644 index 000000000..75ffa45e2 --- /dev/null +++ b/docs/start/integrations/how-it-works.mdx @@ -0,0 +1,174 @@ +--- +title: "How it works" +sidebarTitle: "How it works" +description: "The data model, who mints the ids, when a session ends, and how events reach Cloud." +icon: "workflow" +--- + +Read this once and the framework pages become obvious. + +## Everything is pairs + +An event never arrives alone. One opens a span, one closes it, and the closing event carries a duration the SDK measures from the opening one. + +| Opens | Closes | The closing event carries | +| --- | --- | --- | +| `agent_start` | `agent_end` | `outcome`, `summary` | +| `model_request` | `model_response` | tokens, `stop_reason`, latency | +| `tool_use` | `tool_result` | `output` or `error`, duration | +| `hook_triggered` | `hook_completed` | `outcome`, duration | +| `agent_pause` | `agent_resume` | how long the pause lasted | +| `human_wait` | `human_input` | the answer, and how long the person took | + +Because the SDK measures those durations itself, passing `duration_ms` to a closing event raises a `ValueError`. The exception is `model_response`, where only you know the real provider latency. + + + An opening event with no closing one is a span that never finishes. The session renders as still running, forever, and its active duration keeps growing. This is the failure mode to watch for when you instrument by hand. + + +## How a session ends + +**There is no session-end event.** A session is not something you close — it is a group of events sharing a `session_id`. + +Status is derived from the shape of the trace: + +| Status | When | +| --- | --- | +| `ongoing` | At least one span is still open | +| `paused` | An `agent_pause` has no matching `agent_resume` | +| `error` | Nothing is open, and at least one event failed | +| `done` | Nothing is open, and nothing failed | + +So a session ends when every pair is closed. The adapters emit `agent_end` for you, and on teardown they close anything still open and mark it incomplete — a crashed run settles as `done` with a visible gap rather than hanging. + + + This is why a session can span two calls. A LangGraph `interrupt()` pauses the run, the root span deliberately stays open, and the resuming call closes it. Both calls are one session. + + +## Who mints which id + +| Id | Minted by | Notes | +| --- | --- | --- | +| `session_id` | You, or the SDK | `session("chat-42")` is used verbatim; omitted, the SDK generates a `uuid4().hex` | +| `agent_id` | You, or the framework | From `agent("analyst")`, a CrewAI `role`, a `FunctionAgent.name`. A UUID-looking value is refused and replaced | +| `tool_call_id`, `hook_id`, `request_id` | You, or the framework | Adapters reuse the framework's own run ids, which is why pairs survive thread hops | +| **Event id** | **Cloud, at ingest** | The SDK emits none | +| **`dedup_key`** | **Cloud, at ingest** | A hash of org, session, timestamp, type and payload. This is the real identity — it makes a retried batch collapse instead of duplicating | + +Adapters resolve `session_id` in a defined order — an explicit option, then per-call metadata, then the enclosing scope, then framework metadata, then the framework's own run id. It is never invented when one of those exists, because a synthesized id splits one run across several sessions. + +Keep `agent_id` low cardinality. It is the primary facet on every dashboard surface, and it is a `LowCardinality(String)` column — a per-run value in it degrades the column and fills the filter dropdown with one entry per run. + +Adapters defend that column for you: a label that is a UUID or a long bare hex string becomes `main`, and a readable name carrying a per-run id is stripped back to the readable part, so `agent-3f9a1c2b-…` records as `agent`. The real id is kept on `fw_agent_id` / `fw_run_id`, where it is queryable without being a facet. A name that merely contains short segments like `agent-v2` or `step-3` is left alone. + +The guard is for labels the *framework* chose. An `agent_id` you pass yourself — to `event.*`, or to `failproofai_sdk.agent(...)` — is taken exactly as given, because silently rewriting an explicit argument would be worse than the cardinality it prevents. Name your own spans accordingly. + +## How events reach Cloud + +```mermaid +flowchart LR + A["Your agent"] --> B["Adapter"] + B --> C["Writer
in-memory queue"] + C -->|"every 0.5s"| D["Spool
JSONL on disk"] + D --> E["Failproof daemon"] + E -->|"HTTPS"| F["Cloud"] +``` + +| Stage | Job | Runs in | +| --- | --- | --- | +| Adapter | Translates a framework callback into one of 15 event types | Your process | +| Writer | Queues, batches, writes JSONL atomically | Your process, background thread | +| Spool | Durable handoff, survives your process exiting | Local disk | +| Daemon | Watches the spool, ships batches, deletes what it shipped | Your machine | +| Ingest | Assigns a row id and dedup key, promotes queryable columns | Cloud | + +The spool is what makes this safe: your agent never blocks on the network, and a Cloud outage means a growing directory rather than lost events. + +Each flush writes one batch file, `.tmp` first, then `fsync`, then an atomic rename: + +```text +~/.failproofai/custom-agents/events/ + event-2026-08-20T10-15-00-123Z-48213-0.jsonl +``` + +The daemon only picks up `.jsonl`, so it can never read a half-written file. The stem carries a timestamp, process id and sequence number, so two processes flushing in the same millisecond cannot collide. The queue is capped at 10,000 events; past that it drops the oldest and logs. + + + **The daemon ships your batches; it does not rewrite them.** `collector.redact` scrubs credential-shaped strings out of the events the daemon captures *itself* — CLI session transcripts and hook activity — and it is applied where those events are written, not where batches are shipped. A batch the SDK wrote goes to Cloud exactly as your process produced it, so a prompt or a tool argument that contains a key still contains it on arrival. + + That is deliberate: these are your own instrumentation calls, and rewriting their payloads in transit would mean the events you receive are not the events you emitted. It does mean the SDK is where you control payload contents — `capture_content=False` on the adapter, or simply not passing the secret to `input=` — and that setting `collector.redact` is not a substitute for either. + + + + Do not inspect the spool to confirm delivery while the daemon is running. It deletes each batch within milliseconds, so a listing races the collector and shows far fewer events than were emitted — which looks exactly like an SDK that recorded nothing. + + +## What is in the package + +Installing `failproofai-sdk` installs everything, all four adapters included. The extras pull in the **framework**, not the adapter. + +```python +import failproofai_sdk # loads nothing outside the standard library +failproofai_sdk.instrument() # imports only the adapters you actually need +``` + +`import failproofai_sdk` is contractually zero-dependency, enforced by a test that installs the built wheel with `--no-deps` and another that proves no framework reaches `sys.modules`. + + + There is no `failproofai_sdk.crewai` attribute. Adapters are deliberately not exposed on the top-level package: touching one would import the framework as a side effect of an attribute access, breaking the zero-dependency promise. Use `instrument()`. + + +```python +failproofai_sdk.instrument() # every framework already imported +failproofai_sdk.instrument("crewai") # exactly one, by name +failproofai_sdk.uninstrument("crewai") # put it back +``` + +| Name | Also accepts | +| --- | --- | +| `langchain` | `langgraph`, `langchain_core` | +| `crewai` | — | +| `llama_index` | `llamaindex`, `llama-index` | +| `pydantic_ai` | `pydantic-ai`, `pydanticai` | + +Auto-detection reads `sys.modules`, not the installed package list, so a framework you have installed but never imported is not instrumented and is never imported on your behalf. To see what is wired up: + +```python +from failproofai_sdk.integrations import active, available + +available() # ('crewai', 'langchain', 'llama_index', 'pydantic_ai') +active() # ('langchain',) +``` + + + `instrument("crewai")` on a machine without CrewAI raises an `ImportError` whose message contains the exact install command. Instrumenting is an explicit request, so silently doing nothing would be the wrong answer. + + + + **Call `instrument()` after you import your framework, not before.** Because auto-detection reads `sys.modules`, a bare `instrument()` above the `import langchain` line finds nothing, installs nothing and returns `()` — the process then runs with the SDK imported, the adapter apparently installed, and not one event emitted. It logs a warning saying exactly this, so check your logs if a run records nothing; naming the framework explicitly, `instrument("langchain")`, works from anywhere because it imports the adapter on request. + + +## When instrumentation fails + +Every callback runs inside a wrapper whose only job is to re-raise, so your call sits in exactly one `try` and everything the SDK does happens outside it. + +| What happens | Result | +| --- | --- | +| A hook raises | Logged once with its traceback. Your call is unaffected | +| The same hook raises three times | That one hook is disabled for the rest of the process, with one error line | +| `FAILPROOFAI_SDK_STRICT=1` is set | The exception is re-raised instead | +| A framework version is outside the tested range | Warns once, instruments anyway | +| A single capability is missing | That one hook is disabled, never the whole adapter | + +The default is right in production and wrong while debugging, because it can only ever prove "it did not crash". Set `FAILPROOFAI_SDK_STRICT=1` to make a swallowed failure loud. + +## Next + + + + LangGraph, CrewAI, LlamaIndex, Pydantic AI, and custom agents. + + + Configuration, the event catalog, and correlation rules. + + diff --git a/docs/start/integrations/langchain.mdx b/docs/start/integrations/langchain.mdx new file mode 100644 index 000000000..e7d1b7f68 --- /dev/null +++ b/docs/start/integrations/langchain.mdx @@ -0,0 +1,220 @@ +--- +title: "LangChain and LangGraph" +sidebarTitle: "LangChain and LangGraph" +description: "Instrument graphs, nodes, tools, retrievers, and model calls with one call." +icon: "share-2" +--- + +One adapter serves both. LangGraph runs on `langchain-core`'s callback manager, so instrumenting one instruments the other. + +## Install + +```bash +pip install 'failproofai-sdk[langgraph]' +``` + +For LangChain without LangGraph, use `failproofai-sdk[langchain]`. + +Supported: `langchain-core` 1.4.7 to 2.0, `langgraph` 1.2 to 2.0. Outside that range the adapter still installs and warns once. + +## Instrument + +```python +import failproofai_sdk + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +with failproofai_sdk.session(): + graph.invoke({"messages": [HumanMessage("...")]}) +``` + +`instrument()` registers a tracer through `langchain_core.tracers.context.register_configure_hook`. LangChain injects it into every callback manager it builds, so graphs, tools, and models are captured without changing a call site — including ones inside libraries you did not write. + +## What gets recorded + +| LangChain or LangGraph | Failproof event | +| --- | --- | +| Root run | `agent_start`, `agent_end` | +| LangGraph node | `hook_triggered`, `hook_completed` | +| Compiled subgraph | Nested `agent_start`, `agent_end` | +| Tool run | `tool_use`, `tool_result` | +| Retriever run | `tool_use`, `tool_result`, output summarized | +| Chat model or LLM run | `model_request`, `model_response`, with token usage | +| Streamed tokens | Folded into the response as chunk count and time to first token. Token counts need `ChatOpenAI(stream_usage=True)` — see below | +| `interrupt()` | `human_wait`, `agent_pause` | +| `Command(resume=...)` | `agent_resume`, `human_input`, correlated on the `Interrupt.id` — including when the resume happens in a different process against the same checkpointer | +| Unhandled exception | `error`, then `agent_end` with outcome `failed` | + +A node becomes a hook rather than a nested agent. `agent_id` is the primary facet across every dashboard surface, so promoting `retrieve`, `grade_documents`, and `should_continue` to agents would drown that facet and label the session after whichever node ran first. Hook spans render the same way and give you a per-node latency view. + + + **Name your nodes whatever you like.** A node's own run is identified by its shape — a non-leaf run carrying LangGraph's own step tag — not by its name, so `add_node("lookup_population", ToolNode([...]))` records the tool, and `add_node("ChatOpenAI", ...)` records the model call. Naming a node after the thing it runs used to make that thing's events disappear. + + + + **Streaming and token counts.** `.stream()` / `.astream()` do not emit per-token events — they fold into the closing `model_response` as `fw_chunks` and `fw_ttft_ms`. Token counts are a separate matter: OpenAI only sends usage on a streamed response when asked, so `model_response` carries no tokens unless you construct the model as `ChatOpenAI(..., stream_usage=True)`. The adapter records what the framework gives it; without that flag there is nothing to record. + + +## Example + +```python +import failproofai_sdk +from langchain_core.messages import HumanMessage +from langchain_core.tools import tool +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import ToolNode, create_react_agent + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + + +@tool +def price_of(item: str) -> float: + """Return the unit price of an item in USD.""" + return {"widget": 42.0, "gadget": 17.5}[item.lower().strip()] + + +@tool +def stock_of(item: str) -> int: + """Return the units of an item currently in stock.""" + return {"widget": 120, "gadget": 0}[item.lower().strip()] + + +tools = ToolNode([price_of, stock_of], handle_tool_errors=True) +graph = create_react_agent(ChatOpenAI(model="gpt-4o-mini"), tools) + +with failproofai_sdk.session(): + with failproofai_sdk.agent("analyst", goal="price and stock report"): + result = graph.invoke({ + "messages": [HumanMessage("Price and stock for widget and gadget?")] + }) +``` + +## Name your spans + +By default the root span takes the graph's own name. Wrap it to get a label you chose: + +```python +with failproofai_sdk.session(): + with failproofai_sdk.agent("analyst", goal="price and stock report"): + graph.invoke(...) +``` + +For multi-agent setups, nest the scopes. Each worker becomes a child span carrying `parent_id`: + +```python +with failproofai_sdk.session(): + with failproofai_sdk.agent("supervisor"): + with failproofai_sdk.agent("researcher"): + research_graph.invoke(...) + with failproofai_sdk.agent("writer"): + writer_graph.invoke(...) +``` + +Keep `agent_id` low cardinality. Use a role or node name, never a UUID or a per-run string. + +## Control the session + +The session id resolves in this order, first match winning: + +1. `instrument("langchain", session_id=...)` +2. `config={"metadata": {"failproofai_sdk_session_id": ...}}` +3. The enclosing `failproofai_sdk.session()` scope +4. `metadata["session_id"]`, `metadata["conversation_id"]`, or `metadata["thread_id"]` +5. The root run id + +It is never generated from scratch, because a synthesized id splits one run across several sessions. + +```python +graph.invoke( + {"messages": [...]}, + config={"metadata": {"failproofai_sdk_session_id": f"chat-{user_id}"}}, +) +``` + +## Options + +```python +failproofai_sdk.instrument( + "langchain", + session_id=None, # pin every run to one session id + include_chains=set(), # allowlist intermediate chains as hook pairs + capture_content=True, # False drops prompts and completions from payloads + graph_callbacks=True, # first-class interrupt and resume, needs langgraph 1.2+ +) +``` + +Set `capture_content=False` for regulated data. Structure, timings, token counts, tool names, and outcomes are still recorded; message bodies are not. + +`include_chains` applies to **nested** runs only. A runnable you invoke at the top level is the session's root, so it becomes the agent span rather than a hook pair, and naming it here has no effect. + +## Human in the loop + +`interrupt()` produces four events, and neither pair is redundant: + +```python +from langgraph.types import Command, interrupt + +def approve(state): + decision = interrupt({"prompt": "Ship it?", "options": ["yes", "no"]}) + return {"approved": decision == "yes"} + +with failproofai_sdk.session(): + graph.invoke(state, config) # human_wait, agent_pause + graph.invoke(Command(resume="yes"), config) # agent_resume, human_input +``` + +`human_wait` to `human_input` carries the prompt and the answer. `agent_pause` to `agent_resume` is the only pair that feeds paused time, so without it a ten-minute human wait is billed as active agent time. The root span stays open across the gap, keeping both calls in one session. + +## Common problems + + + + `create_react_agent` propagates the exception. To let the model see the failure and continue, build the tool node explicitly: + + ```python + from langgraph.prebuilt import ToolNode, create_react_agent + + tools = ToolNode([price_of, stock_of], handle_tool_errors=True) + graph = create_react_agent(model, tools) + ``` + + The failure is recorded as a `tool_result` carrying an error either way. This only decides whether the run survives it. + + + + A direct `llm.invoke()` outside any graph has no parent run, so it opens a root span and emits its model pair inside it. The dashboard parents leaves to an open agent, so the span is deliberate. Name it: + + ```python + with failproofai_sdk.agent("summariser"): + summary = ChatOpenAI(model="gpt-4o-mini").invoke([HumanMessage(text)]) + ``` + + + + You passed a Failproof handler in `config={"callbacks": [...]}` as well as calling `instrument()`. Remove it. The configure hook already covers every callback manager in the process. + + + + They do not. LangGraph raises `GraphInterrupt` through the same path as a real exception, so every pause reaches the tracer as an error callback. Any `GraphBubbleUp` subclass is treated as control flow instead, so an approval does not paint a red error. + + + + Check in this order: `instrument()` ran before the graph executed; there is a `with failproofai_sdk.session():` around the call; `FAILPROOFAI_SDK_STRICT=1` set, so a degraded hook raises instead of being swallowed. + + + +## Next + + + + Pairs, ids, session lifecycle, and delivery. + + + Follow causality through the session you just captured. + + + CrewAI, LlamaIndex, Pydantic AI, and custom agents. + + diff --git a/docs/start/integrations/llamaindex.mdx b/docs/start/integrations/llamaindex.mdx new file mode 100644 index 000000000..90a6a626d --- /dev/null +++ b/docs/start/integrations/llamaindex.mdx @@ -0,0 +1,232 @@ +--- +title: "LlamaIndex" +sidebarTitle: "LlamaIndex" +description: "Instrument workflows, steps, function agents, and retrievers." +icon: "database" +--- + +## Install + +```bash +pip install 'failproofai-sdk[llamaindex]' +``` + +Supported: `llama-index-core` 0.14.23 to 0.15. 0.14.23 is the release where the workflow stream started carrying the typed agent events this adapter reads. Below it, model names and agent structure both go missing. + +## Instrument + +```python +import asyncio + +import failproofai_sdk + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + + +async def main(): + async with failproofai_sdk.session(): + await agent.run("...") + + +asyncio.run(main()) +``` + +LlamaIndex's agent API is async. Every scope works under `async with` as well as `with` and produces identical events. + +`instrument()` attaches an event handler and a span handler to LlamaIndex's global dispatcher. Together they make the agent loop visible, not just its model calls. + + + Without one extra argument on your LLM, every token count in your trace is null. See [Token counts](#token-counts) below. + + +## Token counts + +`FunctionAgent` calls `astream_chat`, and `llama-index-llms-openai` does not send `stream_options={"include_usage": True}` when it streams. The provider therefore never sends the usage chunk, and there is nothing for any instrumentation to read. + +This is upstream LlamaIndex behavior. Opt in on your LLM: + +```python +from llama_index.llms.openai import OpenAI + +llm = OpenAI( + model="gpt-4o-mini", + additional_kwargs={"stream_options": {"include_usage": True}}, +) +``` + +Measured on the same run and model: + +| | Input tokens | Output tokens | +| --- | --- | --- | +| Without | `null` | `null` | +| With | 148 | 17 | + +Non-streaming calls (`llm.chat`, `llm.achat`) report usage with no configuration. Only the streaming path, which is the default agent path, needs this. + +## What gets recorded + +| LlamaIndex | Failproof event | +| --- | --- | +| `Workflow.run` root span | Session, `agent_start`, `agent_end` | +| Nested `Workflow.run` span | Nested `agent_start`, `agent_end` | +| Workflow step span | `hook_triggered`, `hook_completed` | +| LLM chat start and end | `model_request`, `model_response` | +| `FunctionTool.call` span | `tool_use`, `tool_result` | +| Retrieval start and end | `tool_use`, `tool_result`, output summarized | +| Embeddings | Nothing, unless `embeddings=True` | +| A tool waiting on a person | `human_wait`, `agent_pause`, then `agent_resume`, `human_input` | +| `AgentWorkflow` handoff | A nested `agent_start`, `agent_end` per agent, parented to the workflow | +| Exception | `error`, then `agent_end` with outcome `failed`, and `agent_end.summary` naming it | +| `handler.cancel_run()` | `agent_end` with outcome `cancelled` and no `error` — a stop button is not a failure | + +`agent_id` is the `FunctionAgent.name` when you set one, and the workflow class name otherwise. Under an `AgentWorkflow`, each agent that takes a turn gets its own nested span under the workflow, so a handoff reads as two agents rather than one. + +Retrieval output is summarized rather than dumped. A retriever returns documents, and storing them in the payload would put your corpus in the events store once per query. The count, score range, and truncated snippets are kept instead. + +## Example + +```python +import asyncio + +import failproofai_sdk +from llama_index.core.agent.workflow import FunctionAgent +from llama_index.core.tools import FunctionTool +from llama_index.llms.openai import OpenAI + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +POP = {"tokyo": "37M", "delhi": "33M"} +AREA = {"tokyo": "2,194 km2", "delhi": "1,484 km2"} + + +def population(city: str) -> str: + """Population of a city. Valid: tokyo, delhi.""" + return POP.get(city.lower().strip(), "unknown") + + +def area(city: str) -> str: + """Land area of a city. Valid: tokyo, delhi.""" + return AREA.get(city.lower().strip(), "unknown") + + +async def main(): + agent = FunctionAgent( + name="city_analyst", + tools=[ + FunctionTool.from_defaults(fn=population), + FunctionTool.from_defaults(fn=area), + ], + llm=OpenAI( + model="gpt-4o-mini", + additional_kwargs={"stream_options": {"include_usage": True}}, + ), + system_prompt="Use the tools. Be terse.", + ) + + async with failproofai_sdk.session(): + async with failproofai_sdk.agent("city_analyst", goal="compare two cities"): + print(await agent.run("Compare Tokyo and Delhi on population and area.")) + + +asyncio.run(main()) +``` + +The agent loop appears in the trace as hook pairs: `init_run`, `setup_agent`, `run_agent_step`, `parse_agent_output`, `call_tool`, and `aggregate_tool_results`. They are the framework's own loop, so they are hooks rather than agents, which keeps `agent_id` meaningful. + +## Name your spans + +`agent_id` is the `FunctionAgent.name` when you set one, and the workflow class name otherwise. + +```python +FunctionAgent(name="city_analyst", tools=[...], llm=llm) # agent_id = "city_analyst" +``` + +In an `AgentWorkflow`, that name is also what the handoff is recorded under: the workflow is the parent span and each agent that takes a turn opens its own nested one, so `agent_id` tells you which agent did the work and `parent_id` tells you which workflow it belonged to. An agent handed control back later opens a second turn rather than reopening the first. + +Wrap the run to override it, or to group several agents under one parent: + +```python +async with failproofai_sdk.agent("research", goal="compare two cities"): + await agent.run(...) +``` + +Keep `agent_id` low cardinality. It is the primary facet on every dashboard surface, so use a role or workflow name, never a UUID or per-run string. + +## Control the session + +This adapter takes **no `session_id` option**. The session comes from the enclosing scope, and otherwise a generated `uuid4().hex` per workflow run: + +```python +async with failproofai_sdk.session(f"chat-{user_id}"): + await agent.run(...) +``` + +## Options + +```python +failproofai_sdk.instrument( + "llama_index", + embeddings=False, # True records embedding calls as tool pairs + steps=True, # False drops workflow-step hook pairs + capture_messages=True, # False drops prompts and system text from payloads + stale_after=600.0, # seconds before an abandoned span is force-closed + reaper_interval=30.0, # how often the reaper sweeps; 0 disables it +) +``` + +| Option | Why you would change it | +| --- | --- | +| `embeddings` | Turn on only when debugging embedding latency or cost. A bulk index build is thousands of calls and will bury the timeline. | +| `steps` | Turn off if you only want model and tool events and find the agent loop noisy. | +| `capture_messages` | Turn off for regulated data. Structure, timings, tokens, and outcomes are still recorded. | +| `stale_after` | A workflow that never finishes leaves an open span. The reaper force-closes it after this many seconds so the session settles instead of reading `ongoing` forever. | +| `reaper_interval` | Sweep frequency. Set to `0` to disable the reaper entirely. | + +## Human in the loop + +Captured when the wait happens inside a tool: + +```python +async def ask_human(question: str) -> str: + """Ask a person and wait for their answer.""" + response = await ctx.wait_for_event(HumanResponseEvent) + return response.answer +``` + +`ctx.wait_for_event` in a plain workflow step is not captured. The runtime catches the drop before it reaches the dispatcher, so the step exits and re-runs later with no signal to key a pause on. The FunctionAgent pattern, which LlamaIndex documents, waits inside a tool and is captured in full. + +## Common problems + + + + Add `additional_kwargs={"stream_options": {"include_usage": True}}` to your LLM. See [Token counts](#token-counts). + + + + There is no standard usage field in LlamaIndex. The adapter tries several known shapes, and an integration naming its counters something new will not match. The raw dict always ships, so look at `usage` in the payload to see what your provider called them. A populated `usage` with empty token columns is deliberate: it beats a confident wrong number. + + + + That is the FunctionAgent loop, one set per iteration. Filter by hook name on the dashboard. These step timings are usually the reason to use this adapter rather than a model-only one. + + + + Check in this order: `instrument()` ran before the run; there is an `async with failproofai_sdk.session():` around the `await`; `llama-index-core` is 0.14.23 or newer; `FAILPROOFAI_SDK_STRICT=1` set, so a degraded hook raises instead of being swallowed. + + + +## Next + + + + Pairs, ids, session lifecycle, and delivery. + + + Follow causality through the session you just captured. + + + LangGraph, CrewAI, Pydantic AI, and custom agents. + + diff --git a/docs/start/integrations/pydantic-ai.mdx b/docs/start/integrations/pydantic-ai.mdx new file mode 100644 index 000000000..23f4d0bba --- /dev/null +++ b/docs/start/integrations/pydantic-ai.mdx @@ -0,0 +1,209 @@ +--- +title: "Pydantic AI" +sidebarTitle: "Pydantic AI" +description: "Instrument typed agents, tools, model calls, and retries." +icon: "badge-check" +--- + +## Install + +```bash +pip install 'failproofai-sdk[pydantic-ai]' +``` + +Supported: `pydantic-ai-slim` 2.0 to 3.0. 2.0 removed `Agent(instrument=...)` and introduced the capability protocol this adapter is built on, so 1.x cannot be instrumented this way. + +## Instrument + +```python +import failproofai_sdk +from pydantic_ai import Agent + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() # before constructing any Agent + +agent = Agent("openai:gpt-4o-mini", system_prompt="Be terse.") + +with failproofai_sdk.session(): + result = agent.run_sync("...") +``` + + + `instrument()` must run before you construct an `Agent`. The capability is appended at construction, so an agent built earlier carries none and records nothing, with no error because nothing went wrong. This is the most common cause of an empty trace with this adapter. + + +Module-scope agents are where this bites: + +```python +# agents.py +agent = Agent("openai:gpt-4o-mini") # constructed at import time + +# main.py +import failproofai_sdk +failproofai_sdk.instrument() # run this FIRST +import agents # now the agent gets the capability +``` + +Confirm it took: + +```python +print([type(c).__name__ for c in agent.root_capability.capabilities]) +# ['FailproofAI', 'ToolSearch', 'PendingMessageDrainCapability'] +``` + +Pydantic AI merges the list you pass into a single `root_capability`, so there is +no `agent.capabilities` attribute to read. + +Agents built while instrumented keep the capability, so you can `uninstrument()` and re-instrument without rebuilding them. + +## What gets recorded + +| Pydantic AI | Failproof event | +| --- | --- | +| Agent run | `agent_start`, `agent_end` | +| Model request | `model_request`, `model_response`, with token usage | +| Tool call | `tool_use`, `tool_result`, with the arguments the model sent | +| `ModelRetry` from a tool | `tool_result` carrying an error | +| Unhandled exception | `error`, then `agent_end` with outcome `failed` | + +There is no hook pair and no human-in-the-loop pair here. Pydantic AI has no node or step boundary to bracket and no built-in human pause, so there is nothing to map. If you build either, emit the events yourself — see [Custom agents](/start/integrations/custom-agents). + +`output_type` makes no difference to the trace. A typed run and a string run produce the same events. + +## Example + +```python +import failproofai_sdk +from pydantic import BaseModel +from pydantic_ai import Agent, ModelRetry + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +PRICE = {"widget": 42.0, "gadget": 17.5} +STOCK = {"widget": 120, "gadget": 0} + + +class Report(BaseModel): + headline: str + out_of_stock: list[str] + + +agent = Agent( + "openai:gpt-4o-mini", + output_type=Report, + system_prompt="Use the tools for every number. If a tool fails, note it and continue.", +) + + +@agent.tool_plain +def price_of(item: str) -> float: + """Unit price of an item. Valid: widget, gadget.""" + return PRICE[item.lower().strip()] + + +@agent.tool_plain +def stock_of(item: str) -> int: + """Units in stock. Valid: widget, gadget.""" + return STOCK[item.lower().strip()] + + +@agent.tool_plain +def restock_eta(item: str) -> str: + """Restock ETA. Not available.""" + raise ModelRetry(f"no restock schedule for {item!r} — answer without it") + + +with failproofai_sdk.session(): + with failproofai_sdk.agent("inventory", goal="stock report"): + result = agent.run_sync( + "For widget and gadget, get price and stock. " + "For anything out of stock, try the restock ETA. Then produce the report." + ) +``` + +In the trace, `restock_eta` appears as a `tool_result` carrying an error, followed by another model call where the agent works around it, and the run still ends `success`. Both facts are kept. + +## Errors, retries, and control flow + +Pydantic AI raises exceptions for three different things, and the adapter separates them: + +| Exception | Treated as | Result | +| --- | --- | --- | +| `ModelRetry`, `ToolRetryError`, `ToolFailedError` | A real tool failure | `tool_result` with an error; the run can still end `success` | +| `SkipToolExecution`, `SkipToolValidation`, `SkipModelRequest`, `CallDeferred`, `ApprovalRequired` | Control flow | Not an error; the run is being steered | +| Anything else | A failure | `error`, then `agent_end` with outcome `failed` | + +`ModelRetry` is in the first group deliberately. It means an attempt genuinely failed and the model was asked to try again, which is what a tool span's error field is for. Classifying it as control flow would hide real tool failures behind a green run. + +## Name your spans + +Pydantic AI's own run span is named `agent`. Wrap the call to give it a label you chose: + +```python +with failproofai_sdk.session(): + with failproofai_sdk.agent("inventory", goal="stock report"): + agent.run_sync("...") +``` + +The framework's span then nests under `inventory`, and that is where the model and tool events hang. + +Keep `agent_id` low cardinality. It is the primary facet on every dashboard surface, so use a role name, never a UUID or per-run string. + +## Control the session + +Resolved in this order, first match winning: + +1. `instrument("pydantic_ai", session_id=...)` +2. The enclosing `failproofai_sdk.session()` scope +3. The run's `conversation_id`, then its `run_id` +4. A generated `uuid4().hex` + +```python +with failproofai_sdk.session(f"chat-{user_id}"): + agent.run_sync("...") +``` + +## Options + +```python +failproofai_sdk.instrument( + "pydantic_ai", + session_id=None, # pin every run to one session id + capture_content=True, # False drops prompts and completions from payloads +) +``` + +## Common problems + + + + The `Agent` was constructed before `instrument()` ran. See the warning above, and check `agent.root_capability.capabilities`. + + + + A bare `raise` propagates; that is Pydantic AI's design. To let the model work around it, raise `ModelRetry` with a message it can act on. The failure is recorded either way. + + + + That child is Pydantic AI's own run span, and it is where the model and tool events hang. Drop your own scope if you want a single span, at the cost of the custom name. + + + + Pydantic AI's async graph stack is longer than the payload field limit, and a traceback's last line is the exception itself. This field is trimmed from the front rather than the back, so the line you need survives. + + + +## Next + + + + Pairs, ids, session lifecycle, and delivery. + + + Follow causality through the session you just captured. + + + LangGraph, CrewAI, LlamaIndex, and custom agents. + + diff --git a/sdk/python/README.md b/sdk/python/README.md index d1e386881..f4a4c3b71 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -38,6 +38,63 @@ A daemon on the same host watches that directory and uploads each batch. Either spool root. If no daemon is running, batches simply accumulate on disk — the SDK does not fail, and your agent does not block. +## Agent frameworks + +If your agent runs on LangChain/LangGraph, CrewAI, LlamaIndex or Pydantic AI, +one line captures it — runs, sub-agents, tools, model calls and their token +counts — without threading an id through anything: + +```python +import failproofai_sdk + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() # auto-detects what is already imported + +graph.invoke({"messages": [...]}) # unchanged +``` + +```bash +pip install 'failproofai-sdk[langgraph]' # or [langchain] [crewai] [llamaindex] [pydantic-ai] +``` + +The adapter code ships in the base wheel and imports its framework lazily, so +the extras are a convenience — `pip install failproofai-sdk` still declares no +dependencies at all, and `import failproofai_sdk` loads nothing outside the +standard library. See `skill/references/frameworks.md` for the per-framework +mapping, and `docs/` for a per-framework integration guide with runnable +examples beside it. + +## Scopes + +The same identity layer, for code the adapters do not cover. `session_id` and +`agent_id` are optional on every event method — omitted, they resolve from the +enclosing scope: + +```python +with failproofai_sdk.session() as sid: + with failproofai_sdk.agent("planner", goal=question): + + with failproofai_sdk.tool_call("search", input={"q": q}) as t: + t.output = search(q) # tool_use / tool_result, timed + + with failproofai_sdk.agent("writer"): # a sub-agent; parent inferred + failproofai_sdk.event.model_request(model="...") +``` + +`agent()` brackets a run with `agent_start`/`agent_end` and records an `error` +before the end event when the block raises — a cancellation closes it as +`cancelled` rather than failed. Every scope works under `async with` too. + +Contextvars do **not** cross into a new thread, so hand work over with +`propagate`: + +```python +pool.submit(failproofai_sdk.propagate(work), item) +``` + +Nothing bound and nothing passed raises `TypeError` naming the fix. It is never +a silent emit: ingest skips an event with no session and answers `200`. + ## Quick start ```python diff --git a/sdk/python/docs/README.md b/sdk/python/docs/README.md new file mode 100644 index 000000000..314dcd43e --- /dev/null +++ b/sdk/python/docs/README.md @@ -0,0 +1,249 @@ +# failproofai-sdk — integration guide + +Plug your agent into Failproof AI. One call, no call-site changes, no ids +threaded through your code. + +```bash +pip install 'failproofai-sdk[langgraph]' +``` + +```python +import failproofai_sdk + +failproofai_sdk.instrument() # auto-detects the frameworks you imported + +with failproofai_sdk.session(): + graph.invoke(...) # recorded +``` + +That is the whole integration. + +--- + +## pick your framework + +| framework | guide | runnable code | +|---|---|---| +| LangChain / LangGraph | [langgraph/](langgraph/) | [langgraph/examples/](langgraph/examples/) | +| CrewAI | [crewai/](crewai/) | [crewai/examples/](crewai/examples/) | +| LlamaIndex | [llama_index/](llama_index/) | [llama_index/examples/](llama_index/examples/) | +| Pydantic AI | [pydantic_ai/](pydantic_ai/) | [pydantic_ai/examples/](pydantic_ai/examples/) | +| **no framework** (your own agent) | [manual/](manual/) | [manual/examples/](manual/examples/) | + +Using something else — AutoGen, Haystack, Semantic Kernel, your own loop? Read +[manual/](manual/). It is the same fidelity, it just costs you the call sites, +and it explains why AutoGen has no adapter. + +--- + +## what you get + +Here is a real trace, printed by `langgraph/examples/quickstart.py`. This is +captured output, not an illustration: + +``` +━━ failproof_ai · langgraph quickstart + session faf3a02d64464e34809eae95fa244230 + events 14 · agents 1 · types 8 + + № offset event detail + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 1 +0.000s agent_start LangGraph + 2 +0.001s hook_triggered agent + 3 +0.002s model_request gpt-5.6-terra + 4 +3.023s model_response gpt-5.6-terra · 21 out-tok + 5 +3.024s hook_completed agent + 6 +3.024s hook_triggered tools + 7 +3.025s tool_use word_count + 8 +3.025s tool_result word_count · ok + 9 +3.025s hook_completed tools + 10 +3.026s hook_triggered agent + 11 +3.027s model_request gpt-5.6-terra + 12 +5.717s model_response gpt-5.6-terra · 5 out-tok + 13 +5.720s hook_completed agent + 14 +5.721s agent_end LangGraph · success +``` + +One `graph.invoke()` from the outside. Fourteen events with timings, token +counts and the tool call, from the inside. + +--- + +## the three calls + +### 1. `configure()` — optional + +```python +failproofai_sdk.configure( + environment="production", # label on every event; default "dev" + flush_interval=0.5, # seconds between disk flushes + base_dir=None, # spool root; default ~/.failproofai/custom-agents +) +``` + +Defaults are usually right. Set `environment` so you can tell prod from staging +on the dashboard. + +### 2. `instrument()` — the one that does the work + +```python +failproofai_sdk.instrument() # every framework already imported +failproofai_sdk.instrument("crewai") # exactly one +failproofai_sdk.uninstrument() # put everything back +``` + +Auto-detection reads `sys.modules` — a framework you have installed but are not +using is never imported on your behalf. + +### 3. `session()` — group one run + +```python +with failproofai_sdk.session(): + ... +``` + +A session is one run. Without it, each top-level call becomes its own session, +which is almost never what you want. + +--- + +## the fifteen event types + +| group | events | what they mean | +|---|---|---| +| agents | `agent_start` `agent_end` | a unit of work begins / ends | +| | `agent_pause` `agent_resume` | it is blocked / unblocked (feeds paused time) | +| models | `model_request` `model_response` | one LLM round trip, with tokens | +| tools | `tool_use` `tool_result` | one tool call, with args and output | +| hooks | `hook_triggered` `hook_completed` | a node / step / task boundary | +| humans | `human_wait` `human_input` | you asked a person / they answered | +| | `human_pause` `human_interrupt` | a person paused / stopped the agent | +| failures | `error` | something went wrong | + +**Events come in pairs.** The closing event carries a `duration_ms` measured +from the matching opening one. An opening event with no close renders as a span +that never finishes. + +--- + +## what each framework emits + +Measured from the real runs in this directory, not declared. + +| event | langgraph | crewai | llama_index | pydantic ai | your own | +|---|:--:|:--:|:--:|:--:|:--:| +| `agent_start` / `agent_end` | yes | yes | yes | yes | you | +| `model_request` / `model_response` | yes | yes | yes | yes | you | +| `tool_use` / `tool_result` | yes | yes | yes | yes | you | +| `hook_triggered` / `hook_completed` | node | task | step | — | you | +| `error` | yes | yes | yes | yes | automatic | +| `human_wait` / `human_input` | yes | yes | yes | — | you | +| `agent_pause` / `agent_resume` | yes | yes | yes | — | you | +| `human_pause` / `human_interrupt` | — | — | — | — | you | + +A dash means the framework has no such concept, not that it is missing. +Pydantic AI has no node boundary and no built-in human pause, so there is +nothing to map. `human_pause` and `human_interrupt` describe a *person* acting +on the agent — a stop button — which no framework signals; emit those yourself. + +--- + +## running the examples + +Every example in this tree was executed against a live model before it shipped. + +```bash +pip install 'failproofai-sdk[langgraph]' +export OPENAI_API_KEY=sk-... +export FPAI_MODEL=gpt-4o-mini # optional, this is the default + +python docs/langgraph/examples/quickstart.py +``` + +Each one prints the event stream it produced when it finishes. That printer +lives in [`_shared/`](_shared/) and is **cosmetics only** — the two bootstrap +lines at the top of each example exist just to import it. Delete them and the +example still instruments correctly. + +--- + +## instrumentation cannot break your agent + +Every callback goes through a wrapper whose only job is to re-raise. Your call +sits in exactly one `try`; everything the SDK does happens outside it. + +- a hook that raises is logged once, with its traceback, and your call is + unaffected; +- a hook that raises three times at the same site is disabled for the rest of + the process, with one error line saying so. + +That firewall is the right default in production and the wrong one when you are +debugging. To make failures loud instead: + +```bash +export FAILPROOFAI_SDK_STRICT=1 +``` + +Without it you can only ever prove "it did not crash", never "it swallowed the +right thing". + +--- + +## zero dependencies + +`import failproofai_sdk` pulls in nothing outside the standard library. That is +enforced, not asserted: one test installs the built wheel with `--no-deps`, +another launches a fresh interpreter and proves no framework reaches +`sys.modules`. + +The extras install the **framework**, not the adapter — the adapter code always +ships in the base wheel and is imported only when `instrument()` asks for it. +Most people already have the framework and never need an extra. + +| extra | installs | +|---|---| +| `failproofai-sdk[langchain]` | `langchain-core>=1.4.7,<2` | +| `failproofai-sdk[langgraph]` | `langgraph>=1.2,<2` | +| `failproofai-sdk[crewai]` | `crewai>=1.13,<2` | +| `failproofai-sdk[llamaindex]` | `llama-index-core>=0.14.23,<0.15` | +| `failproofai-sdk[pydantic-ai]` | `pydantic-ai-slim>=2.0,<3` | + +There is deliberately no `[all]` — an extra that installs four agent frameworks +at once is a resolver problem handed to somebody who wanted a telemetry library. + +--- + +## verifying it works + +Look at the dashboard, or query the store. + +**Do not read the spool directory to check.** When `failproofaid` is running it +collects and deletes each batch file within milliseconds of it appearing, so a +read races the collector and returns only what has not shipped yet — which looks +exactly like an adapter that emitted nothing but its closing events. This is a +real thing that happened while writing these docs. + +To inspect events in-process instead, tap the writer: + +```python +captured = [] +_original = failproofai_sdk._writer.submit +failproofai_sdk._writer.submit = lambda e: (captured.append(e), _original(e))[1] +``` + +Every example here does exactly that. + +--- + +## troubleshooting + +| symptom | most likely cause | +|---|---| +| no events at all | `instrument()` never ran, or ran after you built the agent (Pydantic AI) | +| events, but no session | no `with failproofai_sdk.session():` — each call became its own session | +| `TypeError: ... missing session_id` | you emitted from a new thread; wrap it in `failproofai_sdk.propagate()` | +| spans that never finish | an opening event with no closing one — emit the pairs | +| null token counts on LlamaIndex | streaming has no usage; see [llama_index/](llama_index/) | +| `agent_id` full of uuids | keep it low-cardinality — a role or node name, never a run id | +| duplicated events on LangChain | you registered a second callback handler as well as `instrument()` | +| adapter seems to do nothing | set `FAILPROOFAI_SDK_STRICT=1` and run again | diff --git a/sdk/python/docs/_shared/README.md b/sdk/python/docs/_shared/README.md new file mode 100644 index 000000000..afb42e387 --- /dev/null +++ b/sdk/python/docs/_shared/README.md @@ -0,0 +1,43 @@ +# _shared + +Cosmetics for the runnable examples. **Nothing here is part of the SDK**, and no +example needs it to instrument correctly. + +Two helpers: + +- `model()` — reads `FPAI_MODEL` / `MODEL`, defaults to `gpt-4o-mini`, so one + export drives every example. +- `trace(session_id)` — prints the event stream the run produced. + +`trace()` is why the examples are worth running rather than reading. An adapter +that "works" is one whose event stream you can see, so every example ends by +printing its own instead of asserting in a comment that events happened. + +## why it taps the writer instead of reading the spool + +When `failproofaid` is running it collects and **deletes** each batch file +within milliseconds of it appearing. A spool read therefore races the collector +and returns only whatever has not shipped yet — which looks exactly like an +adapter that emitted nothing but its closing events. + +That is not hypothetical: it is what happened the first time these examples were +run, and it read as three adapter bugs that did not exist. + +So `capture()` wraps `failproofai_sdk._writer.submit` and appends every entry to +a list first. Same dict that goes to disk, race-free, and it works whether or not +a daemon is running. `banner()` calls it, so every example is tapped from its +first line. + +## removing it + +Each example starts with two bootstrap lines that exist only to import this +package: + +```python +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +... +from _shared import banner, model, trace +``` + +Delete those and the `banner()` / `trace()` calls, and the example still +instruments correctly. It just stops printing. diff --git a/sdk/python/docs/_shared/__init__.py b/sdk/python/docs/_shared/__init__.py new file mode 100644 index 000000000..971e4a169 --- /dev/null +++ b/sdk/python/docs/_shared/__init__.py @@ -0,0 +1,195 @@ +"""Cosmetics shared by the examples. No SDK behaviour lives here. + +Two helpers, both deliberately boring: + +* `model()` — the model id, from `FPAI_MODEL` / `MODEL`, so one export drives + every example instead of a dozen edits. +* `trace()` — reads the spool back and prints what the run actually recorded. + +`trace()` is the point of these examples. An adapter that "works" is one whose +event stream you can read, so every example ends by printing its own trace +rather than asserting in a comment that events were produced. It reads the same +JSONL the daemon collects, filtered to the session just run. + +Nothing here is required to use the SDK. Delete this folder and the examples +still instrument correctly; they just stop printing. +""" +from __future__ import annotations + +import json +import os +import pathlib +import sys + +import failproofai_sdk + +# ── brand ──────────────────────────────────────────────────────────────────── +# Exact hex from the design system: pink #e4587d, mint #66d1b5, ink #d8d6d2. +# Truecolor, and only when stdout is a terminal — a piped log gets clean text. +_TTY = sys.stdout.isatty() + + +def _c(code: str, text: str) -> str: + return f"\033[{code}m{text}\033[0m" if _TTY else text + + +def pink(t: str) -> str: + return _c("38;2;228;88;125", t) + + +def mint(t: str) -> str: + return _c("38;2;102;209;181", t) + + +def dim(t: str) -> str: + return _c("38;2;120;120;130", t) + + +def bold(t: str) -> str: + return _c("1", t) + + +RULE = "━" * 68 + + +def model() -> str: + """The model id every example runs against.""" + return os.environ.get("FPAI_MODEL") or os.environ.get("MODEL") or "gpt-4o-mini" + + +def banner(title: str, subtitle: str = "") -> None: + """Print the example's header and start capturing events.""" + capture() + print() + print(pink("━━ ") + bold(title.lower())) + if subtitle: + print(dim(" " + subtitle)) + print() + + +# ── capturing what the run emitted ─────────────────────────────────────────── +# NOT by reading the spool. When `failproofaid` is running it collects and +# DELETES each batch file within milliseconds of it appearing, so a spool read +# races the daemon and returns whatever happens to be left — which looks exactly +# like an adapter that only emitted its closing events. (That is a real thing +# that happened while writing these examples.) +# +# So the tap sits on the writer instead: the same dict that goes to disk is +# appended to a list first. Race-free, and it works whether or not a daemon is +# running. +_CAPTURED: list[dict] = [] +_TAPPED = False + + +def capture() -> None: + """Start recording every event this process emits. Idempotent.""" + global _TAPPED + if _TAPPED: + return + writer = failproofai_sdk._writer + original = writer.submit + + def _tee(entry: dict) -> None: + _CAPTURED.append(entry) + return original(entry) + + writer.submit = _tee # instance attribute; the real method is untouched + _TAPPED = True + + +def events(session_id: str | None = None) -> list[dict]: + """Everything captured, optionally narrowed to one session.""" + rows = [r for r in _CAPTURED if session_id is None or r.get("session_id") == session_id] + return sorted(rows, key=lambda r: r.get("timestamp", "")) + + +_LEAF = { + "tool_use": "tool_result", + "model_request": "model_response", + "hook_triggered": "hook_completed", + "agent_start": "agent_end", + "human_wait": "human_input", + "agent_pause": "agent_resume", +} +_CLOSERS = set(_LEAF.values()) + + +def _detail(row: dict) -> str: + t = row["type"] + if t in ("tool_use", "tool_result"): + bits = [row.get("tool_name") or "?"] + if row.get("error"): + bits.append(pink("error")) + elif t == "tool_result": + bits.append(mint("ok")) + return " · ".join(bits) + if t in ("model_request", "model_response"): + bits = [row.get("model") or "?"] + tok = row.get("output_tokens") + if tok: + bits.append(f"{tok} out-tok") + if row.get("error"): + bits.append(pink("error")) + return " · ".join(bits) + if t in ("agent_start", "agent_end"): + bits = [row.get("agent_id") or "?"] + if row.get("parent_id"): + bits.append(dim("under " + row["parent_id"])) + if row.get("outcome"): + good = row["outcome"] == "success" + bits.append((mint if good else pink)(row["outcome"])) + return " · ".join(bits) + if t in ("hook_triggered", "hook_completed"): + return " · ".join(x for x in (row.get("hook_name"), row.get("status")) if x) + if t == "error": + return pink(str(row.get("message", ""))[:48]) + for key in ("prompt", "reason", "response", "summary", "goal"): + if row.get(key): + return dim(str(row[key])[:48]) + return "" + + +def trace(session_id: str, *, title: str = "trace") -> list[dict]: + """Print the event stream this run produced. Returns the rows.""" + rows = events(session_id) + if not rows: + print(pink(" no events — is the spool configured?")) + return rows + + kinds = sorted({r["type"] for r in rows}) + agents = sorted({r.get("agent_id") for r in rows if r.get("agent_id")}) + t0 = rows[0]["timestamp"] + + print(pink("━━ ") + bold(f"failproof_ai · {title}")) + print(dim(f" session {session_id}")) + print(dim(f" events {len(rows)} · agents {len(agents)} · types {len(kinds)}")) + print() + print(dim(f" {'№':>3} {'offset':>8} {'event':<16} detail")) + print(dim(" " + RULE)) + + depth = 0 + for i, row in enumerate(rows, 1): + t = row["type"] + if t in _CLOSERS: + depth = max(depth - 1, 0) + indent = " " * depth + offset = _offset(t0, row["timestamp"]) + name = f"{indent}{t}" + print(f" {i:>3} {dim(offset):>8} {name:<16} {_detail(row)}") + if t in _LEAF: + depth += 1 + print(dim(" " + RULE)) + print(dim(" " + " ".join(kinds))) + print() + return rows + + +def _offset(first: str, ts: str) -> str: + from datetime import datetime + + try: + a = datetime.fromisoformat(first.replace("Z", "+00:00")) + b = datetime.fromisoformat(ts.replace("Z", "+00:00")) + return f"+{(b - a).total_seconds():.3f}s" + except Exception: + return "" diff --git a/sdk/python/docs/crewai/README.md b/sdk/python/docs/crewai/README.md new file mode 100644 index 000000000..6d0ad958f --- /dev/null +++ b/sdk/python/docs/crewai/README.md @@ -0,0 +1,328 @@ +# CrewAI + +- [Install](#install) +- [The integration](#the-integration) +- [How it attaches](#how-it-attaches) +- [What gets recorded](#what-gets-recorded) +- [A complete example](#a-complete-example) +- [Agent names come from roles](#agent-names-come-from-roles) +- [Options](#options) +- [Human in the loop](#human-in-the-loop) +- [Pitfalls](#pitfalls) +- [Runnable examples](#runnable-examples) + +--- + +## Install + +```bash +pip install 'failproofai-sdk[crewai]' # pins crewai >=1.13,<2 +``` + +**Supported:** `crewai` 1.13 → 2.0. 1.13 is a capability floor, not a guess — it +is the release that added `started_event_id` and normalised token usage, both of +which the adapter relies on to pair events and report tokens. + +--- + +## The integration + +```python +import failproofai_sdk + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +with failproofai_sdk.session(): + Crew(agents=[analyst, writer], tasks=[gather, summarise]).kickoff() +``` + +Nothing about your crew, agents, tasks or tools changes. + +--- + +## How it attaches + +`instrument()` constructs a listener on CrewAI's module-level +`crewai_event_bus` and subscribes one handler per event class. + +Two details that matter if you are debugging: + +**The bus dispatches by exact type, with no MRO walk.** There is no `BaseEvent` +catch-all to subscribe to, so the adapter carries an explicit table of every +class it maps. A CrewAI release that renames an event class disables exactly +that one hook (with a warning) rather than breaking the adapter. + +**Every handler is `async def`, deliberately.** The bus dispatches *sync* +handlers onto a ten-worker pool where submission order is not execution order. +Measured on crewai 1.15.8, sync handlers produced a demonstrably wrong event +stream — pairs closing before they opened. Async handlers run in order. + +--- + +## What gets recorded + +| CrewAI | Failproof event | When | +|---|---|---| +| crew kickoff | `agent_start` / `agent_end` | the crew starts and finishes | +| flow start / finish | `agent_start` / `agent_end` | a flow you wrote | +| agent execution | nested `agent_start` / `agent_end` | `agent_id` = the **role** | +| task | *nothing* — recorded as a link | see below | +| flow method | `hook_triggered` / `hook_completed` | each `@start` / `@listen` method | +| guardrail | `hook_triggered` / `hook_completed` | each LLM guardrail | +| tool usage | `tool_use` / `tool_result` | each tool call | +| memory query / save / retrieve | `tool_use` / `tool_result` | named `memory.query`, `memory.save`, … | +| knowledge query / retrieve | `tool_use` / `tool_result` | named `knowledge.query`, … | +| LLM call | `model_request` / `model_response` | each model call, with usage | +| LLM stream chunk | *nothing* | folded into `fw_chunks`, `fw_ttft_ms` | +| human feedback requested | `human_wait` + `agent_pause` | the flow blocks on a person | +| human feedback received | `agent_resume` + `human_input` | the person answers | +| agent execution error | `error` + `agent_end(outcome="failed")` | the agent raises | + +### Why a task emits nothing + +A CrewAI task is a subset of the agent execution that runs it. Emitting both +would double every row in the timeline and render them as siblings, which is +simply wrong — the task *contains* the agent work, it does not run beside it. + +The task is still recorded: it is registered as a link so its children resolve +to the crew above them, and its id and name ride along on the agent's own events +as `fw_task_id` and `fw_task_name`. You can filter by task; you just do not get +a duplicate span. + +### Why memory and knowledge ops are tools + +They are retrieval calls the agent makes. Naming them for the surface they hit +(`memory.query`, `knowledge.retrieve`) rather than the class that fired means +they show up on the tools page next to your real tools, where you actually want +to compare their latency. + +--- + +## A complete example + +```python +import failproofai_sdk +from crewai import Agent, Crew, Process, Task +from crewai.tools import tool + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +MODEL = "openai/gpt-4o-mini" + +METRICS = { + "revenue": "$4.2M ARR, up 12% QoQ", + "churn": "3.1% monthly logo churn, up from 2.4%", + "nps": "41, flat", +} + + +@tool("lookup_metric") +def lookup_metric(name: str) -> str: + """Look up a business metric by name. Valid: revenue, churn, nps.""" + return METRICS.get(name.lower().strip(), "unknown metric") + + +analyst = Agent( + role="analyst", # <- this becomes agent_id + goal="pull the numbers that matter and state them plainly", + backstory="You read dashboards for a living and distrust round numbers.", + tools=[lookup_metric], + llm=MODEL, +) +writer = Agent( + role="writer", + goal="turn numbers into three lines an exec will actually read", + backstory="You write board updates. You never pad.", + llm=MODEL, +) + +gather = Task( + description="Look up 'revenue', 'churn' and 'nps' with the tool.", + expected_output="Three lines, one metric each.", + agent=analyst, +) +summarise = Task( + description="Using the metrics above, write a three-line exec summary.", + expected_output="Exactly three lines.", + agent=writer, + context=[gather], +) + +with failproofai_sdk.session(): + result = Crew( + agents=[analyst, writer], + tasks=[gather, summarise], + process=Process.sequential, + ).kickoff() + +print(result) +``` + +This produces **17–18 events across 6 types**, with the handoff visible: + +``` + 1 +0.000s agent_start crew + 2 +0.049s agent_start analyst · under crew + 3 +0.055s model_request gpt-5.6-terra + 4 +3.112s model_response gpt-5.6-terra · 65 out-tok + 5 +3.114s tool_use lookup_metric + ... + 13 +5.129s agent_end analyst · success + 14 +5.149s agent_start writer · under crew + 15 +5.156s model_request gpt-5.6-terra + 16 +8.177s model_response gpt-5.6-terra · 48 out-tok + 17 +8.205s agent_end writer · success + 18 +8.224s agent_end crew · success +``` + +Two roles, two spans, one session — you can now break latency and token spend +down per role. + +--- + +## Agent names come from roles + +`agent_id` is taken from `Agent(role=...)`. That is what makes it a useful +dashboard facet instead of a UUID nobody can read. + +```python +Agent(role="analyst", ...) # agent_id = "analyst" good +Agent(role="analyst-7f3a2b", ...) # one facet entry per run bad +``` + +`agent_id` is a `LowCardinality(String)` column. A role containing a run id or a +timestamp degrades that column for every query anyone runs. Keep roles short, +human, and stable across runs. + +If a role looks like an id (a UUID, a long hex string), the adapter refuses it +and falls back to a safe default, putting the real value in `fw_agent_id`. + +--- + +## Options + +```python +failproofai_sdk.instrument( + "crewai", + session_id=None, # pin every run to one session id +) +``` + +`session_id` is the only option this adapter reads — there is no +`capture_content` here. Prompts and completions are always recorded, truncated +to the payload budget. + +Session identity resolves as: the `session_id` option, then the enclosing +`failproofai_sdk.session()` scope, then a generated `uuid4().hex` per crew or +flow. + +--- + +## Human in the loop + +CrewAI's flow runtime emits `HumanFeedbackRequestedEvent` before it blocks on a +person and `HumanFeedbackReceivedEvent` after the answer. Both are recorded, as +four events: + +``` +human_wait prompt, options +agent_pause starts the paused-time clock + ... a person is reading ... +agent_resume stops it — this is what feeds paused time +human_input the answer, with the wait measured +``` + +Neither pair is redundant. `human_wait` → `human_input` carries the prompt and +the answer; `agent_pause` → `agent_resume` is the only thing that feeds paused +time. Without the second pair, a ten-minute human wait is billed as ten minutes +of active agent time. + +> **CrewAI sets no correlation id on either event.** `request_id` is `None` on +> both and `started_event_id` is `None` on the received one, so there is nothing +> to join on directly. The adapter pairs on `request_id` when present (the +> enterprise async provider does set it), then on `(flow_name, method_name)`, +> then on the most recently opened pause. The last fallback is sound because a +> console prompt blocks — two cannot interleave. **If you build your own +> concurrent feedback provider, set `request_id` on both events.** + +Feedback arriving for a request that was never seen — a flow resumed in another +process, say — records the answer but deliberately withholds `agent_resume`, +because closing a pause that never opened would subtract an interval that was +never added. + +--- + +## Pitfalls + +### Roles with ids in them wreck the facet + +**Symptom:** the agent filter on the dashboard has thousands of entries. + +**Cause:** `role` contains a UUID, a timestamp or a per-run suffix. + +**Fix:** use a stable human role. Put the run-specific id in the task +description or a payload field. + +--- + +### Asserting on events straight after `kickoff()` finds nothing + +**Symptom:** your test reads zero events, but the dashboard shows them. + +**Cause:** the bus is asynchronous. `kickoff()` returns before the last handlers +have run. + +**Fix:** + +```python +from crewai.events.event_bus import crewai_event_bus + +crew.kickoff() +crewai_event_bus.flush(timeout=30) # now read +``` + +This is a property of CrewAI, not of the SDK. + +--- + +### A crew that dies mid-tool leaves an open span + +**Symptom:** a session shows as `ongoing` forever. + +**Cause:** `agent_end` force-closes open *pauses* but not tools or models, so a +run that dies inside a tool call leaves that `tool_use` unclosed. + +**Fix:** none needed in normal operation — `uninstrument()` and process teardown +close whatever is still open, marking it `fw_incomplete` with +`outcome="cancelled"`. If you are killing processes with `SIGKILL`, nothing can +run, and the span stays open. + +--- + +### Nothing is recorded at all + +1. Did `instrument()` run before `kickoff()`? +2. Is there a `with failproofai_sdk.session():` around it? +3. Set `FAILPROOFAI_SDK_STRICT=1` and run again — a degraded hook will raise + instead of being swallowed. +4. Check your `crewai` version is ≥ 1.13. Below that, `started_event_id` does + not exist and pairing falls back to a heuristic. + +--- + +## Runnable examples + +| file | what it shows | events | +|---|---|---| +| [`examples/quickstart.py`](examples/quickstart.py) | one agent, one task, one tool | 10 | +| [`examples/research_crew.py`](examples/research_crew.py) | 2 agents, 2 tasks, sequential handoff | 17 | + +```bash +export OPENAI_API_KEY=sk-... +export FPAI_MODEL=gpt-4o-mini +python docs/crewai/examples/quickstart.py +``` + +Both were run against a live model before shipping. diff --git a/sdk/python/docs/crewai/examples/quickstart.py b/sdk/python/docs/crewai/examples/quickstart.py new file mode 100644 index 000000000..f0b69e288 --- /dev/null +++ b/sdk/python/docs/crewai/examples/quickstart.py @@ -0,0 +1,61 @@ +"""crewai — the smallest thing that produces a trace. + + pip install 'failproofai-sdk[crewai]' + python docs/crewai/examples/quickstart.py + +`instrument()` registers a listener on crewai's event bus, so the crew, each +agent and each tool call are recorded. The task gets no span of its own — it is +the agent execution that runs it, and rides along on that agent's events as +`fw_task_id`/`fw_task_name`. `agent_id` comes from the agent's `role`, which is +what makes it a useful dashboard facet rather than a uuid nobody can read. +""" +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from crewai import Agent, Crew, Task +from crewai.tools import tool + +failproofai_sdk.configure(environment="examples") +failproofai_sdk.instrument() + +MODEL = f"openai/{model()}" + + +@tool("lookup_metric") +def lookup_metric(name: str) -> str: + """Look up a business metric by name.""" + return {"revenue": "$4.2M", "churn": "3.1%"}.get(name, "unknown") + + +def main() -> None: + banner("crewai quickstart", "one agent, one task, one tool") + + analyst = Agent( + role="analyst", + goal="find the revenue number", + backstory="You read metrics.", + tools=[lookup_metric], + llm=MODEL, + ) + task = Task( + description="Look up 'revenue' with the tool.", + expected_output="The revenue figure.", + agent=analyst, + ) + + with failproofai_sdk.session() as sid: + result = Crew(agents=[analyst], tasks=[task]).kickoff() + + print(" answer:", str(result).strip()[:120]) + trace(sid, title="crewai quickstart") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/docs/crewai/examples/research_crew.py b/sdk/python/docs/crewai/examples/research_crew.py new file mode 100644 index 000000000..40dcb022c --- /dev/null +++ b/sdk/python/docs/crewai/examples/research_crew.py @@ -0,0 +1,92 @@ +"""crewai — two agents, two tasks, a shared tool, sequential handoff. + + pip install 'failproofai-sdk[crewai]' + python docs/crewai/examples/research_crew.py + +What this demonstrates that the quickstart does not: + +* two `agent_id` values in one session, taken from each agent's `role`, so the + dashboard can break latency and token spend down per role; +* two tasks that add no spans of their own — a crewai task *is* the agent + execution that runs it, so recording both would double every row and render + them as siblings. Each task rides along on its agent's events instead, as + `fw_task_id`/`fw_task_name`, which you can still filter by; +* a second task consuming the first one's output, so the handoff is visible in + the trace rather than implied. +""" +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from crewai import Agent, Crew, Process, Task +from crewai.tools import tool + +failproofai_sdk.configure(environment="examples") +failproofai_sdk.instrument() + +MODEL = f"openai/{model()}" + +_METRICS = { + "revenue": "$4.2M ARR, up 12% QoQ", + "churn": "3.1% monthly logo churn, up from 2.4%", + "nps": "41, flat", +} + + +@tool("lookup_metric") +def lookup_metric(name: str) -> str: + """Look up a business metric by name. Valid names: revenue, churn, nps.""" + return _METRICS.get(name.lower().strip(), "unknown metric") + + +def main() -> None: + banner("crewai research crew", "2 agents · 2 tasks · sequential handoff") + + analyst = Agent( + role="analyst", + goal="pull the numbers that matter and state them plainly", + backstory="You read dashboards for a living and distrust round numbers.", + tools=[lookup_metric], + llm=MODEL, + ) + writer = Agent( + role="writer", + goal="turn numbers into three lines an exec will actually read", + backstory="You write board updates. You never pad.", + llm=MODEL, + ) + + gather = Task( + description=( + "Look up 'revenue', 'churn' and 'nps' with the tool. " + "Report each verbatim, one per line." + ), + expected_output="Three lines, one metric each.", + agent=analyst, + ) + summarize = Task( + description="Using the metrics above, write a three-line exec summary.", + expected_output="Exactly three lines.", + agent=writer, + context=[gather], + ) + + with failproofai_sdk.session() as sid: + result = Crew( + agents=[analyst, writer], + tasks=[gather, summarize], + process=Process.sequential, + ).kickoff() + + print(" summary:", str(result).strip()[:220], "\n") + trace(sid, title="crewai research crew") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/docs/langgraph/README.md b/sdk/python/docs/langgraph/README.md new file mode 100644 index 000000000..e6e17d00b --- /dev/null +++ b/sdk/python/docs/langgraph/README.md @@ -0,0 +1,481 @@ +# LangChain / LangGraph + +One adapter serves both — LangGraph runs on `langchain-core`'s callback manager, +so instrumenting one instruments the other. + +- [Install](#install) +- [The integration](#the-integration) +- [How it attaches](#how-it-attaches) +- [What gets recorded](#what-gets-recorded) +- [A complete example](#a-complete-example) +- [Naming your agents](#naming-your-agents) +- [Controlling the session](#controlling-the-session) +- [Options](#options) +- [What an event actually looks like](#what-an-event-actually-looks-like) +- [Human in the loop](#human-in-the-loop) +- [Pitfalls](#pitfalls) +- [Runnable examples](#runnable-examples) + +--- + +## Install + +```bash +pip install 'failproofai-sdk[langgraph]' # pins langgraph >=1.2,<2 +``` + +Using plain LangChain without LangGraph: + +```bash +pip install 'failproofai-sdk[langchain]' # pins langchain-core >=1.4.7,<2 +``` + +You almost certainly have the framework already. The extras exist to state the +supported range; the adapter itself ships in the base wheel. + +**Supported:** `langchain-core` 1.4.7 → 2.0, `langgraph` 1.2 → 2.0. Outside +that range the adapter still installs and warns once, because a version we have +not tested is a better bet than no telemetry. + +--- + +## The integration + +```python +import failproofai_sdk + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +with failproofai_sdk.session(): + graph.invoke({"messages": [HumanMessage("...")]}) +``` + +Three lines, and nothing else in your codebase changes. No decorators on your +nodes, no callback passed to `.invoke()`, no ids threaded through your +functions. + +`instrument()` with no argument auto-detects every supported framework already +imported. To be explicit: + +```python +failproofai_sdk.instrument("langchain") # "langgraph" is an alias +``` + +--- + +## How it attaches + +`instrument()` registers a tracer through +`langchain_core.tracers.context.register_configure_hook`. LangChain injects it +into **every callback manager it builds**, which means: + +- every graph, node, tool, retriever and model call is captured; +- so is anything inside a library you did not write, as long as it goes through + LangChain; +- there is nothing to pass to `.invoke()` and no call site to change. + +The tracer subclasses `BaseTracer`, so LangChain assembles the run tree and +hands over `Run` objects with inputs, outputs, metadata and timings already +collected — two override points instead of twenty hand-correlated callbacks. + +
+Why we do not patch BaseCallbackManager.__init__ + +Some other integrations do, and it double-records. +`BaseCallbackManager.merge()` builds a new manager with handlers already passed +in, so their `isinstance` dedup misses and the handler is added twice. MLflow +patches `merge` as well to work around it. The configure hook has no such hole. + +This is also why you must **not** pass a Failproof handler in +`config={"callbacks": [...]}` yourself — see [Pitfalls](#pitfalls). +
+ +
+Why the handler runs inline + +`run_inline = True` is not optional. `AsyncCallbackManager` dispatches sync +handlers through `run_in_executor` unless a handler opts out, and that hop can +**reorder callbacks** — which scrambles timestamp order and breaks every pairing. +Writing an event is a `deque.append`, so running inline on the event loop is +safe. +
+ +--- + +## What gets recorded + +| LangChain / LangGraph | Failproof event | When | +|---|---|---| +| root run | `agent_start` / `agent_end` | the outermost `.invoke()` / `.stream()` | +| LangGraph node | `hook_triggered` / `hook_completed` | each node entry and exit | +| compiled subgraph | nested `agent_start` / `agent_end` | a subgraph run, named `root/node` | +| tool run | `tool_use` / `tool_result` | each tool call, with args and output | +| retriever run | `tool_use` / `tool_result` | output summarised, not dumped | +| chat model / LLM run | `model_request` / `model_response` | each model call, with token usage | +| streamed tokens | *nothing* | folded into `fw_chunks`, `fw_ttft_ms` on the response | +| `interrupt()` | `human_wait` + `agent_pause` | the graph suspends for a person | +| `Command(resume=...)` | `agent_resume` + `human_input` | the graph continues | +| unhandled exception | `error` + `agent_end(outcome="failed")` | the run raises | +| intermediate chains | *nothing* | unless named in `include_chains` | + +### Why a node is a hook and not a nested agent + +`agent_id` is a `LowCardinality(String)` column and the primary facet on every +dashboard surface. Promoting `retrieve`, `grade_documents` and `should_continue` +to agents would: + +- drown that facet with one entry per node, and +- label the whole session after whichever node happened to run first. + +Hook spans render structurally identically in the timeline, and you get a +per-node latency page for free. Subgraphs *do* become nested agents, because a +compiled subgraph is a genuine unit of work. + +### Why streamed tokens emit nothing + +A 500-token response would otherwise be 500 stored rows against a five-lane +rail. The chunk count, time-to-first-token and a `fw_streamed` flag land on the +single `model_response` instead. + +--- + +## A complete example + +Copy-pasteable, no helpers: + +```python +import failproofai_sdk +from langchain_core.messages import HumanMessage +from langchain_core.tools import tool +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import ToolNode, create_react_agent + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + + +@tool +def price_of(item: str) -> float: + """Return the unit price of an item in USD.""" + return {"widget": 42.0, "gadget": 17.5}[item.lower().strip()] + + +@tool +def stock_of(item: str) -> int: + """Return the units of an item currently in stock.""" + return {"widget": 120, "gadget": 0}[item.lower().strip()] + + +# handle_tool_errors=True feeds a raised exception back to the model as a tool +# message instead of aborting the graph. The failure is recorded either way. +tools = ToolNode([price_of, stock_of], handle_tool_errors=True) +graph = create_react_agent(ChatOpenAI(model="gpt-4o-mini"), tools) + +with failproofai_sdk.session(): + with failproofai_sdk.agent("analyst", goal="price and stock report"): + result = graph.invoke({ + "messages": [HumanMessage("Price and stock for widget and gadget?")] + }) + +print(result["messages"][-1].content) +``` + +Running the fuller version of this +([`examples/research_agent.py`](examples/research_agent.py)) produces **36 +events across 8 types**: the `analyst` span, the graph nested inside it, a hook +pair per node, a model pair per turn with token counts, and a tool pair per call +— including one that failed: + +``` + 1 +0.000s agent_start analyst + 2 +0.001s agent_start LangGraph · under analyst + 3 +0.002s hook_triggered agent + 4 +0.003s model_request gpt-5.6-terra + 5 +3.690s model_response gpt-5.6-terra · 99 out-tok + ... + 28 +5.830s tool_use restock_eta + 29 +5.834s tool_result restock_eta · error + ... + 36 +9.209s agent_end analyst · success +``` + +Note event 29: a failed tool is a `tool_result` carrying `error`. The run still +ends `success`, because the agent recovered — the trace preserves both facts. + +--- + +## Naming your agents + +By default the root span takes the graph's own name (`LangGraph`, or whatever +you passed to `.compile(name=...)`). Wrap it to get something meaningful: + +```python +with failproofai_sdk.session(): + with failproofai_sdk.agent("analyst", goal="price and stock report"): + graph.invoke(...) +``` + +Everything inside now carries `parent_id="analyst"`, and the dashboard groups by +`analyst` rather than by a framework class name. + +For multi-agent setups, nest the scopes — each worker becomes a child span: + +```python +with failproofai_sdk.session(): + with failproofai_sdk.agent("supervisor"): + with failproofai_sdk.agent("researcher"): + research_graph.invoke(...) + with failproofai_sdk.agent("writer"): + writer_graph.invoke(...) +``` + +See [`examples/supervisor_handoff.py`](examples/supervisor_handoff.py) — 38 +events, 5 agents, correctly nested. + +> **Keep `agent_id` low-cardinality.** It is a facet column. Use a role or node +> name (`analyst`, `researcher`), never a UUID or a per-run string. Put the real +> id in a payload field instead. + +--- + +## Controlling the session + +The session id is resolved in this order — first match wins: + +1. `failproofai_sdk.instrument("langchain", session_id="...")` — pins every run +2. `config={"metadata": {"failproofai_sdk_session_id": "..."}}` — per call +3. the ambient `failproofai_sdk.session()` / `agent()` scope +4. `metadata["session_id"]`, `metadata["conversation_id"]`, `metadata["thread_id"]` +5. the root run id + +It is **never synthesised from scratch**, because a made-up id splits one run +into many sessions — a silent wrong answer rather than a loud one. + +Per-call override: + +```python +graph.invoke( + {"messages": [...]}, + config={"metadata": {"failproofai_sdk_session_id": f"chat-{user_id}"}}, +) +``` + +> `thread_id` **is** available to callbacks on this stack, despite what you may +> have read. Reports that it is `None` are about `langchain-core` dropping +> `configurable` from `metadata`; LangGraph 1.2 re-adds it. It is still only the +> fourth resolution step, because a thread is a conversation, not necessarily a +> single run. + +--- + +## Options + +```python +failproofai_sdk.instrument( + "langchain", + session_id=None, # pin every run to one session id + include_chains=set(), # allowlist intermediate chains as hook pairs + capture_content=True, # False drops prompts and completions from payloads + graph_callbacks=True, # first-class interrupt/resume (needs langgraph >= 1.2) +) +``` + +**`capture_content=False`** is the switch for regulated data. Structure, +timings, token counts, tool names and outcomes are all still recorded; the +message bodies and completions are not. + +**`include_chains`** takes a set of runnable names. Use it sparingly — it exists +for a chain that is genuinely a step in your pipeline, not to surface +`RunnableSequence`. + +It applies to **nested** runs only. A runnable you invoke at the top level is +the session's root, so it becomes the agent span rather than a hook pair, and +naming it here has no effect. + +--- + +## What an event actually looks like + +A real `model_response`, captured from `examples/quickstart.py`: + +```json +{ + "timestamp": "2026-08-19T19:50:56.241974Z", + "session_id": "7e5de1571fa14424aa1a5bbd88cb420d", + "agent_id": "LangGraph", + "type": "model_response", + "environment": "production", + "model": "gpt-5.6-terra", + "stop_reason": "tool_calls", + "input_tokens": 139, + "output_tokens": 21, + "content": "", + "role": "assistant", + "request_id": "01a01b93-91a0-7c92-bf45-3857ee43d3ff", + "usage": { "input_tokens": 139, "output_tokens": 21, "total_tokens": 160 }, + "duration_ms": 5202, + "framework": "langchain", + "framework_version": "1.5.6", + "integration_version": "0.0.1b14", + "fw_langgraph_version": "1.2.11", + "fw_run_id": "01a01b93-91a0-7c92-bf45-3857ee43d3ff", + "fw_node": "agent", + "fw_step": 1 +} +``` + +Everything prefixed `fw_` is framework-specific detail, namespaced so it can +never collide with — and silently overwrite — a first-class field like +`model` or `output_tokens`. `request_id` is what pairs this response with its +request; without it the dashboard falls back to FIFO pairing and concurrent +calls mis-pair. + +--- + +## Human in the loop + +LangGraph's `interrupt()` produces **four** events, and neither pair is +redundant: + +```python +from langgraph.types import interrupt, Command + +def approve(state): + decision = interrupt({"prompt": "Ship it?", "options": ["yes", "no"]}) + return {"approved": decision == "yes"} + +with failproofai_sdk.session(): + graph.invoke(state, config) # human_wait + agent_pause + # ... minutes pass ... + graph.invoke(Command(resume="yes"), config) # agent_resume + human_input +``` + +- `human_wait` → `human_input` carries the prompt, the options, the answer and + the pending-human count; +- `agent_pause` → `agent_resume` is the **only** thing that feeds paused time. + Without it the whole human wait is billed as active agent time. + +The root agent span deliberately stays **open** across the gap, so the two +`.invoke()` calls are one session and one run. + +--- + +## Pitfalls + +### A raising tool aborts the whole graph + +**Symptom:** your run dies on the first tool exception instead of recovering. + +**Cause:** `create_react_agent` propagates it by default. + +**Fix:** build the tool node explicitly. + +```python +from langgraph.prebuilt import ToolNode, create_react_agent + +tools = ToolNode([price_of, stock_of], handle_tool_errors=True) +graph = create_react_agent(model, tools) +``` + +The failure is recorded as `tool_result` with `error` either way — this only +decides whether the run survives it. + +--- + +### A bare `llm.invoke()` shows up as an agent named after the model class + +**Symptom:** an `agent_id` of `ChatOpenAI` in your trace. + +**Cause:** a direct model call outside any graph has no parent run, so it opens +a root agent span **and** emits its own `model_request` / `model_response` pair +inside it. The dashboard parents leaves to an open agent and synthesises a +never-ending root span when there is none, so the span is deliberate. + +**Fix:** name it. + +```python +with failproofai_sdk.agent("summariser"): + summary = ChatOpenAI(model="gpt-4o-mini").invoke([HumanMessage(text)]) +``` + +> Before v1.0.1-beta.2 this case emitted the agent span and **nothing else** — +> no model name, no token counts, no latency. If you are on an older build, +> upgrade. + +--- + +### Interrupts look like errors and are not + +**Symptom:** you expect a red error on every human approval. + +**Cause:** LangGraph's runnable does `except BaseException: on_chain_error(e); +raise` with no special case for interrupts, so every HITL pause reaches the +tracer as an error callback. + +**Fix:** none needed. Any `GraphBubbleUp` subclass — `GraphInterrupt`, +`NodeInterrupt`, `ParentCommand`, `GraphDrained` — is treated as control flow. +Without that, every approval would paint a red error plus +`agent_end(outcome="failed")`. + +--- + +### Everything is recorded twice + +**Symptom:** every event appears two times. + +**Cause:** you passed a Failproof handler in `config={"callbacks": [...]}` *as +well as* calling `instrument()`. + +**Fix:** remove it. The configure hook already covers every callback manager in +the process. There is never a reason to register a handler by hand. + +--- + +### Nothing is recorded at all + +Work through these in order: + +1. Did `instrument()` actually run before the graph executed? +2. Is there a `with failproofai_sdk.session():` around the call? Without one, + every `.invoke()` becomes its own session — you will have events, but + scattered. +3. Set `FAILPROOFAI_SDK_STRICT=1` and run again. A silently degraded hook will + now raise instead of being swallowed. +4. Are you reading the spool directory to check? Do not — a running + `failproofaid` deletes each batch within milliseconds. Check the dashboard. + +--- + +### Events from a worker thread raise `TypeError` + +**Symptom:** `TypeError` naming `session_id` from inside a thread pool. + +**Cause:** contextvars propagate into asyncio tasks automatically but **not** +into new threads — a thread starts with an empty context. + +**Fix:** + +```python +pool.submit(failproofai_sdk.propagate(work), x) +threading.Thread(target=failproofai_sdk.propagate(work)).start() +``` + +--- + +## Runnable examples + +| file | what it shows | events | +|---|---|---| +| [`examples/quickstart.py`](examples/quickstart.py) | one tool, one turn — the smallest real trace | 14 | +| [`examples/research_agent.py`](examples/research_agent.py) | 3 tools, several turns, one tool that fails and recovers | 36 | +| [`examples/supervisor_handoff.py`](examples/supervisor_handoff.py) | a supervisor delegating to 2 workers, nested spans | 38 | + +```bash +export OPENAI_API_KEY=sk-... +export FPAI_MODEL=gpt-4o-mini +python docs/langgraph/examples/quickstart.py +``` + +Each prints the event stream it produced. All three were run against a live +model before shipping. diff --git a/sdk/python/docs/langgraph/examples/quickstart.py b/sdk/python/docs/langgraph/examples/quickstart.py new file mode 100644 index 000000000..7c4e23f6e --- /dev/null +++ b/sdk/python/docs/langgraph/examples/quickstart.py @@ -0,0 +1,51 @@ +"""langgraph — the smallest thing that produces a trace. + + pip install 'failproofai-sdk[langgraph]' + python docs/langgraph/examples/quickstart.py + +`instrument()` installs a tracer on langchain's global callback manager, so +every graph, node, tool and model call in the process is recorded — including +ones inside libraries you did not write. Nothing below passes an id by hand. +""" +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from langchain_core.messages import HumanMessage +from langchain_core.tools import tool +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import create_react_agent + +failproofai_sdk.configure(environment="examples") +failproofai_sdk.instrument() + + +@tool +def word_count(text: str) -> int: + """Count the words in a piece of text.""" + return len(text.split()) + + +def main() -> None: + banner("langgraph quickstart", "one tool, one turn") + graph = create_react_agent(ChatOpenAI(model=model()), [word_count]) + + # A session groups everything below into one run. Without it each + # `.invoke()` becomes its own session, which is rarely what you want. + with failproofai_sdk.session() as sid: + result = graph.invoke( + {"messages": [HumanMessage("How many words in 'hello brave new world'?")]} + ) + + print(" answer:", result["messages"][-1].content.strip()[:120]) + trace(sid, title="langgraph quickstart") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/docs/langgraph/examples/research_agent.py b/sdk/python/docs/langgraph/examples/research_agent.py new file mode 100644 index 000000000..e566ca41b --- /dev/null +++ b/sdk/python/docs/langgraph/examples/research_agent.py @@ -0,0 +1,86 @@ +"""langgraph — a real multi-step run: three tools, several turns. + + pip install 'failproofai-sdk[langgraph]' + python docs/langgraph/examples/research_agent.py + +What this demonstrates that the quickstart does not: + +* several model turns in one session, each paired `model_request`/`model_response` + with real token counts and latency; +* three distinct tools, each a `tool_use`/`tool_result` pair carrying arguments + and output; +* a tool that raises — recorded as `tool_result` with `error` set, and the run + keeps going, because a failed tool is data, not a crash; +* a named `agent()` scope, so the trace is labelled `analyst` rather than the + graph's generated id. +""" +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from langchain_core.messages import HumanMessage +from langchain_core.tools import tool +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import ToolNode, create_react_agent + +failproofai_sdk.configure(environment="examples") +failproofai_sdk.instrument() + +_PRICES = {"widget": 42.0, "gadget": 17.5, "sprocket": 3.25} +_STOCK = {"widget": 120, "gadget": 0, "sprocket": 8} + + +@tool +def price_of(item: str) -> float: + """Return the unit price of an item in USD.""" + return _PRICES[item.lower().strip()] + + +@tool +def stock_of(item: str) -> int: + """Return the units of an item currently in stock.""" + return _STOCK[item.lower().strip()] + + +@tool +def restock_eta(item: str) -> str: + """Return the restock ETA. Raises for items that are not tracked.""" + # Deliberately unhandled: the trace should show a failed tool call and the + # agent recovering from it, which is the interesting case to observe. + raise LookupError(f"no restock schedule for {item!r}") + + +def main() -> None: + banner("langgraph research agent", "3 tools · multi-turn · one failing call") + + # `handle_tool_errors=True` feeds the exception back to the model as a tool + # message instead of raising out of `.invoke()`. The failproof trace records + # the failure either way — this just lets the run continue so you can watch + # the model recover from it. + tools = ToolNode([price_of, stock_of, restock_eta], handle_tool_errors=True) + graph = create_react_agent(ChatOpenAI(model=model()), tools) + + question = ( + "For 'widget' and 'gadget': give me the unit price and the stock level. " + "For anything out of stock, try to look up the restock ETA. " + "Finish with a two-line summary." + ) + + # `agent("analyst")` names the span. Without it the adapter falls back to the + # graph's own label, which is stable but not meaningful on a dashboard. + with failproofai_sdk.session() as sid: + with failproofai_sdk.agent("analyst", goal="price and stock report"): + result = graph.invoke({"messages": [HumanMessage(question)]}) + + print(" answer:", result["messages"][-1].content.strip()[:200], "\n") + trace(sid, title="langgraph research agent") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/docs/langgraph/examples/supervisor_handoff.py b/sdk/python/docs/langgraph/examples/supervisor_handoff.py new file mode 100644 index 000000000..be873cb58 --- /dev/null +++ b/sdk/python/docs/langgraph/examples/supervisor_handoff.py @@ -0,0 +1,83 @@ +"""langgraph — a supervisor delegating to two workers. + + pip install 'failproofai-sdk[langgraph]' + python docs/langgraph/examples/supervisor_handoff.py + +Multi-agent is where a flat event log stops being readable, so this is the +example to run if you want to see what the parent/child structure buys you. + +Each worker runs inside its own `agent()` scope nested in the supervisor's, so +every event a worker emits carries `parent_id="supervisor"` and `depth=2`. The +dashboard renders that as a tree; a flat log cannot tell you which agent made +which model call. +""" +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from langchain_core.messages import HumanMessage +from langchain_core.tools import tool +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import create_react_agent + +failproofai_sdk.configure(environment="examples") +failproofai_sdk.instrument() + + +@tool +def fetch_incidents(service: str) -> str: + """List recent incidents for a service.""" + return { + "checkout": "2 incidents: 503 spike (12m), payment timeout (4m)", + "search": "1 incident: index lag (31m)", + }.get(service, "no incidents on record") + + +@tool +def oncall_for(service: str) -> str: + """Return who is on call for a service.""" + return {"checkout": "dana", "search": "kim"}.get(service, "unassigned") + + +def worker(name: str, tools: list, prompt: str) -> str: + """One delegated unit of work, bracketed as its own child agent.""" + graph = create_react_agent(ChatOpenAI(model=model()), tools) + with failproofai_sdk.agent(name, goal=prompt[:60]): + out = graph.invoke({"messages": [HumanMessage(prompt)]}) + return out["messages"][-1].content + + +def main() -> None: + banner("langgraph supervisor handoff", "1 supervisor → 2 workers · nested spans") + + with failproofai_sdk.session() as sid: + with failproofai_sdk.agent("supervisor", goal="incident brief for checkout"): + incidents = worker( + "incident_worker", + [fetch_incidents], + "Use the tool to list recent incidents for the 'checkout' service.", + ) + owner = worker( + "roster_worker", + [oncall_for], + "Use the tool to say who is on call for the 'checkout' service.", + ) + + # The supervisor's own model turn, at depth 1 — the two worker turns + # above are at depth 2 and carry parent_id="supervisor". + summary = ChatOpenAI(model=model()).invoke( + [HumanMessage(f"In two lines, brief the on-call.\n{incidents}\n{owner}")] + ) + + print(" brief:", summary.content.strip()[:200], "\n") + trace(sid, title="langgraph supervisor handoff") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/docs/llama_index/README.md b/sdk/python/docs/llama_index/README.md new file mode 100644 index 000000000..138b2ddc1 --- /dev/null +++ b/sdk/python/docs/llama_index/README.md @@ -0,0 +1,332 @@ +# LlamaIndex + +- [Install](#install) +- [The integration](#the-integration) +- [Read this first: token counts](#read-this-first-token-counts) +- [How it attaches](#how-it-attaches) +- [What gets recorded](#what-gets-recorded) +- [A complete example](#a-complete-example) +- [Options](#options) +- [Human in the loop](#human-in-the-loop) +- [Pitfalls](#pitfalls) +- [Runnable examples](#runnable-examples) + +--- + +## Install + +```bash +pip install 'failproofai-sdk[llamaindex]' # pins llama-index-core >=0.14.23,<0.15 +``` + +**Supported:** `llama-index-core` 0.14.23 → 0.15. 0.14.23 is a capability floor: +it is the release where `to_payload()` replaced `to_dict()` and where the +workflow stream started carrying the typed agent events this adapter reads. +Below it, model names and agent structure both go missing. + +--- + +## The integration + +```python +import asyncio +import failproofai_sdk + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +async def main(): + async with failproofai_sdk.session(): + await agent.run("...") + +asyncio.run(main()) +``` + +LlamaIndex's agent API is async. Every Failproof scope works under `async with` +as well as `with`, and produces byte-identical events either way. + +--- + +## Read this first: token counts + +**Without one extra argument, every token count in your trace will be null.** + +`FunctionAgent` — the agent API LlamaIndex documents — calls `astream_chat`. And +`llama-index-llms-openai` does not send `stream_options={"include_usage": True}` +when it streams, so **the provider never sends the usage chunk at all**. +`LLMChatEndEvent.response.raw` arrives with no `usage` key on it, and there is +nothing for any instrumentation to read. + +This is upstream LlamaIndex behaviour, not a Failproof one. Verified by spying +on the dispatcher directly against `llama-index-core` 0.14.23: every single +`LLMChatEndEvent` in a `FunctionAgent` run has usage absent. + +**The fix is one argument on your LLM:** + +```python +from llama_index.llms.openai import OpenAI + +llm = OpenAI( + model="gpt-4o-mini", + additional_kwargs={"stream_options": {"include_usage": True}}, +) +``` + +Measured on the same run, same model: + +| | `input_tokens` | `output_tokens` | +|---|---|---| +| without | `None` | `None` | +| with | `148` | `17` | + +Non-streaming calls (`llm.chat`, `llm.achat`) extract usage correctly with no +configuration at all. It is only the streaming path — which is the default agent +path — that needs this. + +--- + +## How it attaches + +Two handlers on LlamaIndex's global dispatcher: + +- an **event handler** for the typed workflow and LLM events, and +- a **span handler** for the enter/exit spans around workflows, steps and tools. + +Both together are what makes the agent *loop* visible, not just its model calls. +This is the main thing this adapter buys you over a model-only integration: you +can see `parse_agent_output` taking 160ms, or `aggregate_tool_results` running +twice. + +--- + +## What gets recorded + +| LlamaIndex | Failproof event | When | +|---|---|---| +| `Workflow.run` root span | session + `agent_start` / `agent_end` | the outermost run | +| nested `Workflow.run` span | nested `agent_start` / `agent_end` | a sub-workflow | +| workflow step span | `hook_triggered` / `hook_completed` | each step in the agent loop | +| `LLMChatStartEvent` / `EndEvent` | `model_request` / `model_response` | each model call | +| `FunctionTool.call` span | `tool_use` / `tool_result` | each tool call | +| `RetrievalStartEvent` / `EndEvent` | `tool_use` / `tool_result` | output summarised, not dumped | +| embeddings | *nothing* | unless `embeddings=True` | +| `WaitingForEvent` drop | `human_wait` + `agent_pause` | a tool waits on a person | +| exception | `error` + `agent_end(outcome="failed")` | the run raises | + +`agent_id` is the `FunctionAgent.name` when you set one, and the workflow class +name otherwise — never a span id. + +```python +FunctionAgent(name="city_analyst", tools=[...], llm=llm) # agent_id = "city_analyst" +``` + +### Why retrieval output is summarised + +A retriever returns documents. Dumping them into the payload puts your entire +corpus in the events store, one copy per query. The adapter records the count, +the score range and truncated snippets instead. If you need the documents +themselves, they are in your index. + +### Why steps are hooks and not agents + +`init_run`, `setup_agent`, `run_agent_step`, `parse_agent_output`, `call_tool` +and `aggregate_tool_results` are the framework's own loop, not units of work you +wrote. They are hook pairs so `agent_id` stays a meaningful facet instead of +filling up with machinery. + +--- + +## A complete example + +```python +import asyncio + +import failproofai_sdk +from llama_index.core.agent.workflow import FunctionAgent +from llama_index.core.tools import FunctionTool +from llama_index.llms.openai import OpenAI + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +POP = {"tokyo": "37M", "delhi": "33M", "lagos": "16M"} +AREA = {"tokyo": "2,194 km2", "delhi": "1,484 km2", "lagos": "1,171 km2"} + + +def population(city: str) -> str: + """Population of a city. Valid: tokyo, delhi, lagos.""" + return POP.get(city.lower().strip(), "unknown") + + +def area(city: str) -> str: + """Land area of a city. Valid: tokyo, delhi, lagos.""" + return AREA.get(city.lower().strip(), "unknown") + + +async def main(): + agent = FunctionAgent( + name="city_analyst", + tools=[ + FunctionTool.from_defaults(fn=population), + FunctionTool.from_defaults(fn=area), + ], + llm=OpenAI( + model="gpt-4o-mini", + # REQUIRED for token counts on the streaming agent path. + additional_kwargs={"stream_options": {"include_usage": True}}, + ), + system_prompt="Use the tools. Be terse.", + ) + + async with failproofai_sdk.session(): + async with failproofai_sdk.agent("city_analyst", goal="compare two cities"): + answer = await agent.run("Compare Tokyo and Delhi on population and area.") + + print(answer) + + +asyncio.run(main()) +``` + +The quickstart version of this produces **26 events across 8 types**, and the +agent loop is fully visible: + +``` + 1 +0.000s agent_start Agent + 2 +0.001s hook_triggered init_run + 4 +0.548s hook_triggered setup_agent + 6 +0.549s hook_triggered run_agent_step + 7 +0.551s model_request gpt-5.6-terra + 8 +3.546s model_response gpt-5.6-terra + 10 +3.662s hook_triggered parse_agent_output + 12 +3.825s hook_triggered call_tool + 13 +3.825s tool_use city_population + 14 +3.826s tool_result city_population · ok + 16 +3.826s hook_triggered aggregate_tool_results + ... + 26 +6.968s agent_end Agent · success +``` + +--- + +## Options + +```python +failproofai_sdk.instrument( + "llama_index", + embeddings=False, # True records embedding calls as tool pairs + steps=True, # False drops workflow-step hook pairs + capture_messages=True, # False drops prompts and system text from payloads + stale_after=600.0, # seconds before an abandoned span is force-closed + reaper_interval=30.0, # how often the reaper sweeps; 0 disables it +) +``` + +| Option | Why you would change it | +|---|---| +| `embeddings` | Off by default: a bulk index build is thousands of calls and buries the timeline. On for debugging embedding latency or cost. | +| `steps` | Off if you want only model and tool events and find the agent loop noisy. | +| `capture_messages` | Off for regulated data. Structure, timings, tokens and outcomes are still recorded. | +| `stale_after` | A workflow that never finishes leaves an open span. The reaper force-closes it after this many seconds, so the session settles instead of reading `ongoing` forever. | +| `reaper_interval` | Sweep frequency. `0` disables the reaper. | + +**There is no `session_id` option on this adapter, and no `capture_content`.** +Set the session with a scope, and use `capture_messages` for content: + +```python +async with failproofai_sdk.session(f"chat-{user_id}"): + await agent.run(...) +``` + +--- + +## Human in the loop + +Only visible when the wait happens **inside a tool**. + +```python +async def ask_human(question: str) -> str: + """Ask a person and wait for their answer.""" + ctx = ... # your workflow context + response = await ctx.wait_for_event(HumanResponseEvent) + return response.answer +``` + +That produces `human_wait` + `agent_pause`, then `agent_resume` + +`human_input` on the retry. + +`ctx.wait_for_event` in a **plain workflow step** is not captured. The runtime +catches the drop before it reaches the dispatcher: the step simply exits with +`None` and re-runs later, so there is no signal to key a pause on. The +FunctionAgent pattern — which is the one LlamaIndex documents — waits inside a +tool and is captured in full. + +--- + +## Pitfalls + +### Every token count is null + +See [Read this first](#read-this-first-token-counts). One argument on your LLM. + +--- + +### Token counts are null on a non-OpenAI integration + +**Symptom:** `usage` is populated in the payload but `input_tokens` / +`output_tokens` are `None`. + +**Cause:** there is no standard usage field in LlamaIndex. The adapter tries +`response.raw["usage"]`, then `raw["usage_metadata"]`, then +`response.additional_kwargs`, calling `model_dump()` first when `raw` is a +pydantic model. An integration that names its counters something new will not +match. + +**Why it behaves this way:** the top-level token fields are set **only** when a +recognised key is present, but the raw dict always ships as `usage` regardless. +A populated `usage` with blank token columns is the honest outcome, and much +better than a confident wrong number. Look at `usage` in the payload to see what +your provider actually called them, and open an issue with that key name. + +--- + +### The timeline is buried in `init_run` / `setup_agent` noise + +**Symptom:** far more hook pairs than you expected. + +**Cause:** that is the FunctionAgent loop. Every iteration is +`setup_agent` → `run_agent_step` → `parse_agent_output` → +`call_tool` → `aggregate_tool_results`. + +**Fix:** nothing is wrong, but you can filter by `hook_name` on the dashboard. +The step timings are usually the reason people install this adapter rather than +a model-only one. + +--- + +### Nothing is recorded at all + +1. Did `instrument()` run before you constructed the agent? (For LlamaIndex the + order does not matter, unlike Pydantic AI — but check anyway.) +2. Is there an `async with failproofai_sdk.session():` around the `await`? +3. Set `FAILPROOFAI_SDK_STRICT=1` and run again. +4. Check `llama-index-core >= 0.14.23`. + +--- + +## Runnable examples + +| file | what it shows | events | +|---|---|---| +| [`examples/quickstart.py`](examples/quickstart.py) | one tool, one turn, async | 26 | +| [`examples/research_agent.py`](examples/research_agent.py) | 3 tools, several turns, one tool that fails | 64 | + +```bash +export OPENAI_API_KEY=sk-... +export OPENAI_API_BASE=... # note: llama_index reads API_BASE, not BASE_URL +export FPAI_MODEL=gpt-4o-mini +python docs/llama_index/examples/quickstart.py +``` + +Both were run against a live model before shipping. Both set `stream_options`, +so both report real token counts. diff --git a/sdk/python/docs/llama_index/examples/quickstart.py b/sdk/python/docs/llama_index/examples/quickstart.py new file mode 100644 index 000000000..4a31370c2 --- /dev/null +++ b/sdk/python/docs/llama_index/examples/quickstart.py @@ -0,0 +1,62 @@ +"""llama_index — the smallest thing that produces a trace. + + pip install 'failproofai-sdk[llamaindex]' + python docs/llama_index/examples/quickstart.py + +`instrument()` attaches an event handler and a span handler to llama_index's +global dispatcher, so workflow steps arrive as hook pairs and llm calls as +`model_request`/`model_response`. + +The scopes work under `async with` as well as `with`, which matters here because +llama_index's agent api is async. +""" +import asyncio +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from llama_index.core.agent.workflow import FunctionAgent +from llama_index.core.tools import FunctionTool +from llama_index.llms.openai import OpenAI + +failproofai_sdk.configure(environment="examples") +failproofai_sdk.instrument() + + +def city_population(city: str) -> str: + """Population of a city.""" + return {"tokyo": "37M", "delhi": "33M"}.get(city.lower(), "unknown") + + +async def main() -> None: + banner("llama_index quickstart", "one tool, one turn, async") + + agent = FunctionAgent( + tools=[FunctionTool.from_defaults(fn=city_population)], + llm=OpenAI( + model=model(), + # WITHOUT THIS, EVERY TOKEN COUNT IS NULL. + # FunctionAgent calls `astream_chat`, and llama_index does not ask + # OpenAI for usage on a stream — so `LLMChatEndEvent` carries a + # response with no usage on it and there is nothing for any + # instrumentation to read. This is upstream behaviour, not a + # failproof one; the line below is the documented way back. + additional_kwargs={"stream_options": {"include_usage": True}}, + ), + system_prompt="Answer using the tool. Be terse.", + ) + async with failproofai_sdk.session() as sid: + answer = await agent.run("What is the population of Tokyo?") + + print(" answer:", str(answer).strip()[:120]) + trace(sid, title="llama_index quickstart") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/python/docs/llama_index/examples/research_agent.py b/sdk/python/docs/llama_index/examples/research_agent.py new file mode 100644 index 000000000..2d50f4fe8 --- /dev/null +++ b/sdk/python/docs/llama_index/examples/research_agent.py @@ -0,0 +1,93 @@ +"""llama_index — a multi-tool async agent, including a tool that fails. + + pip install 'failproofai-sdk[llamaindex]' + python docs/llama_index/examples/research_agent.py + +What this demonstrates that the quickstart does not: + +* three tools across several turns, each a `tool_use`/`tool_result` pair; +* a tool that raises, recorded as `tool_result` with `error` set while the run + continues; +* llama_index workflow steps as `hook_triggered`/`hook_completed`, so you can + see the agent loop itself and not just its llm calls; +* a named `agent()` scope wrapping an async run — the scopes support + `async with`, so nothing about identity changes when the framework does. +""" +import asyncio +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from llama_index.core.agent.workflow import FunctionAgent +from llama_index.core.tools import FunctionTool +from llama_index.llms.openai import OpenAI + +failproofai_sdk.configure(environment="examples") +failproofai_sdk.instrument() + +_POP = {"tokyo": "37M", "delhi": "33M", "lagos": "16M"} +_AREA = {"tokyo": "2,194 km2", "delhi": "1,484 km2", "lagos": "1,171 km2"} + + +def population(city: str) -> str: + """Population of a city. Valid: tokyo, delhi, lagos.""" + return _POP.get(city.lower().strip(), "unknown") + + +def area(city: str) -> str: + """Land area of a city. Valid: tokyo, delhi, lagos.""" + return _AREA.get(city.lower().strip(), "unknown") + + +def founding_year(city: str) -> str: + """Founding year of a city. Not actually available for any of them.""" + # Deliberately raises: the trace should show a failed tool and the agent + # carrying on without it. + raise LookupError(f"no founding-year record for {city!r}") + + +async def main() -> None: + banner("llama_index research agent", "3 tools · multi-turn · one failing call") + + agent = FunctionAgent( + tools=[ + FunctionTool.from_defaults(fn=population), + FunctionTool.from_defaults(fn=area), + FunctionTool.from_defaults(fn=founding_year), + ], + llm=OpenAI( + model=model(), + # WITHOUT THIS, EVERY TOKEN COUNT IS NULL. + # FunctionAgent calls `astream_chat`, and llama_index does not ask + # OpenAI for usage on a stream — so `LLMChatEndEvent` carries a + # response with no usage on it and there is nothing for any + # instrumentation to read. This is upstream behaviour, not a + # failproof one; the line below is the documented way back. + additional_kwargs={"stream_options": {"include_usage": True}}, + ), + system_prompt=( + "Use the tools. If a tool fails, say so and continue with what you have." + ), + ) + + question = ( + "Compare Tokyo and Delhi on population and area, then try to get the " + "founding year for each. Finish with a two-line comparison." + ) + + async with failproofai_sdk.session() as sid: + async with failproofai_sdk.agent("city_analyst", goal="compare two cities"): + answer = await agent.run(question) + + print(" answer:", str(answer).strip()[:220], "\n") + trace(sid, title="llama_index research agent") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/python/docs/manual/README.md b/sdk/python/docs/manual/README.md new file mode 100644 index 000000000..fe10aaf13 --- /dev/null +++ b/sdk/python/docs/manual/README.md @@ -0,0 +1,491 @@ +# Your own agent — no framework + +For an agent you wrote yourself, or a framework Failproof has no adapter for. +There is nothing to instrument: you emit the events. + +This is a first-class path, not a consolation prize. It is exactly what the four +adapters call underneath — they are translation tables over the same API. + +- [Install](#install) +- [The three scopes](#the-three-scopes) +- [What a scope does on the way out](#what-a-scope-does-on-the-way-out) +- [The fifteen event methods](#the-fifteen-event-methods) +- [A complete example](#a-complete-example) +- [Threads and async](#threads-and-async) +- [Instrumenting an unsupported framework](#instrumenting-an-unsupported-framework) +- [Why there is no AutoGen adapter](#why-there-is-no-autogen-adapter) +- [Pitfalls](#pitfalls) +- [Runnable examples](#runnable-examples) + +--- + +## Install + +```bash +pip install failproofai-sdk +``` + +No extras. Zero dependencies. + +--- + +## The three scopes + +```python +import failproofai_sdk + +failproofai_sdk.configure(environment="production") + +with failproofai_sdk.session() as sid: # identity only — emits nothing + with failproofai_sdk.agent("planner"): # agent_start / agent_end + with failproofai_sdk.tool_call("search", input={"q": q}) as t: + t.output = search(q) # tool_use / tool_result +``` + +| scope | emits | purpose | +|---|---|---| +| `session()` | nothing | binds a session id — groups one run | +| `agent()` | `agent_start` / `agent_end` | brackets a unit of work | +| `tool_call()` | `tool_use` / `tool_result` | brackets one tool, measuring it | + +**Everything inside them can omit `session_id=` and `agent_id=`.** The scopes +bind identity on contextvars and every `event.*` call reads it back. That is the +whole point — threading two ids through every function that might emit an event +is what turns instrumentation into a diff nobody wants to review. + +All three work under `async with` as well as `with`: + +```python +async with failproofai_sdk.session(): + async with failproofai_sdk.agent("planner"): + async with failproofai_sdk.tool_call("search", input={"q": q}) as t: + t.output = await search(q) +``` + +### Nesting agents + +```python +with failproofai_sdk.session(): + with failproofai_sdk.agent("supervisor"): + with failproofai_sdk.agent("researcher"): # parent_id = "supervisor" + ... + with failproofai_sdk.agent("writer"): # parent_id = "supervisor" + ... +``` + +`parent_id` and `depth` are computed from the stack. You never pass them. + +--- + +## What a scope does on the way out + +`agent()` handles exceptions for you: + +| what happened | events emitted | `outcome` | +|---|---|---| +| nothing raised | `agent_end` | `success` | +| `Exception` | `error`, then `agent_end` | `failed` | +| `KeyboardInterrupt` / `SystemExit` | `error`, then `agent_end` | `failed` | +| `CancelledError` / `GeneratorExit` | `agent_end` only | `cancelled` | + +Two deliberate details: + +- **`error` strictly before `agent_end`.** The dashboard closes the span at + `agent_end`, so anything emitted after it is attributed to nothing. +- **A cancellation is not a failure.** Otherwise every cancelled run pollutes + the errors surface. `asyncio.CancelledError` has been a `BaseException` since + Python 3.8, and it is caught and re-raised untouched. + +The exception is **always** re-raised. A scope never swallows. + +`tool_call()` does the same for tools: a raising body produces +`tool_result(error="TypeName: msg")` and **no** `error` event, because a tool +failure the agent loop catches is not a run-level error, and one that propagates +is reported exactly once by the enclosing `agent()`. + +--- + +## The fifteen event methods + +### agents + +```python +failproofai_sdk.event.agent_start(agent_id="planner", goal="find the cheapest flight") +failproofai_sdk.event.agent_end(agent_id="planner", outcome="success", summary="...") +failproofai_sdk.event.agent_pause(pause_id="p1", reason="awaiting approval") +failproofai_sdk.event.agent_resume(pause_id="p1") +``` + +`outcome` is `"failed"`, never `"failure"` — only `error|failed|timeout|rejected` +count as a failure server-side. + +### models + +```python +failproofai_sdk.event.model_request( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "..."}], + request_id="req-1", # optional; pairs the two events explicitly +) +failproofai_sdk.event.model_response( + model="gpt-4o-mini", + response="...", + input_tokens=139, + output_tokens=21, + request_id="req-1", + duration_ms=5202, # int, never float +) +``` + +Pass `request_id` if you make concurrent model calls — without it the dashboard +pairs requests and responses FIFO per agent, and concurrent calls mis-pair. + +### tools + +```python +failproofai_sdk.event.tool_use(tool_name="search", tool_call_id="c1", input={"q": "..."}) +failproofai_sdk.event.tool_result(tool_name="search", tool_call_id="c1", output="...", error=None) +``` + +`tool_call_id` is what pairs them. Prefer the `tool_call()` scope, which +guarantees the pair even when the body raises. + +### hooks + +```python +failproofai_sdk.event.hook_triggered(hook_name="retrieve", hook_id="h1", trigger_event="node") +failproofai_sdk.event.hook_completed(hook_name="retrieve", hook_id="h1", outcome="success") +``` + +Use these for node, step or middleware boundaries — **not** nested `agent()` +calls. `agent_id` is a low-cardinality facet, and one entry per node drowns it. + +### humans + +```python +failproofai_sdk.event.human_wait(input_id="i1", prompt="Approve?", options=["yes", "no"]) +failproofai_sdk.event.human_input(input_id="i1", response="yes") + +failproofai_sdk.event.human_pause(reason="operator paused the run", user_id="dana") +failproofai_sdk.event.human_interrupt(reason="operator stopped the run", at_step="step_3") +``` + +`human_wait`/`human_input` = *the agent asked a person*. +`human_pause`/`human_interrupt` = *a person acted on the agent* — a stop button, +an operator intervention. No framework signals the second pair, so it is always +yours. + +Emit `agent_pause`/`agent_resume` around a human wait too: only that pair feeds +paused time, and without it the wait is billed as active agent time. + +### failures + +```python +failproofai_sdk.event.error( + error_type="TimeoutError", + message="provider timed out after 30s", + traceback="...", +) +``` + +Usually emitted for you by `agent()`. Call it directly for a failure that does +not raise — a validation rejection, a guardrail trip. + +--- + +## A complete example + +A real tool-calling loop against the OpenAI API, with no agent framework at all: + +```python +import json + +import failproofai_sdk +from openai import OpenAI + +failproofai_sdk.configure(environment="production") + +client = OpenAI() +MODEL = "gpt-4o-mini" + +PRICE = {"widget": 42.0, "gadget": 17.5} +STOCK = {"widget": 120, "gadget": 0} + +TOOLS = [ + { + "type": "function", + "function": { + "name": "price_of", + "description": "Unit price of an item.", + "parameters": { + "type": "object", + "properties": {"item": {"type": "string"}}, + "required": ["item"], + }, + }, + }, +] + + +def run_tool(name: str, args: dict) -> str: + if name == "price_of": + return str(PRICE[args["item"].lower().strip()]) + raise LookupError(f"unknown tool {name!r}") + + +def turn(messages: list): + """One LLM call, bracketed by the model pair.""" + failproofai_sdk.event.model_request(model=MODEL, messages=messages) + reply = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS) + usage = reply.usage + failproofai_sdk.event.model_response( + model=MODEL, + response=reply.choices[0].message.content or "", + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + ) + return reply.choices[0].message + + +messages = [ + {"role": "system", "content": "Use the tools for every number. Be terse."}, + {"role": "user", "content": "Price for widget and gadget?"}, +] + +with failproofai_sdk.session(): + with failproofai_sdk.agent("inventory", goal="price report"): + for _ in range(4): # bounded: an unbounded agent loop is its own bug + message = turn(messages) + calls = message.tool_calls or [] + if not calls: + print(message.content) + break + + messages.append(message.model_dump(exclude_none=True)) + for call in calls: + args = json.loads(call.function.arguments or "{}") + # tool_call() emits tool_use now and tool_result on exit, + # turning a raised exception into `error` on the result. + with failproofai_sdk.tool_call( + call.function.name, tool_call_id=call.id, input=args + ) as handle: + handle.output = run_tool(call.function.name, args) + messages.append({ + "role": "tool", + "tool_call_id": call.id, + "content": str(handle.output), + }) +``` + +Produces **14 events across 6 types** — the same shape an adapter would give +you: + +``` + 1 +0.000s agent_start inventory + 2 +0.000s model_request gpt-5.6-terra + 3 +3.962s model_response gpt-5.6-terra · 96 out-tok + 4 +3.962s tool_use price_of + 5 +3.962s tool_result price_of · ok + ... + 12 +3.962s model_request gpt-5.6-terra + 13 +8.459s model_response gpt-5.6-terra · 29 out-tok + 14 +8.459s agent_end inventory · success +``` + +--- + +## Threads and async + +contextvars propagate into asyncio tasks **automatically**. They do **not** +propagate into new threads — a thread starts with an empty context. + +```python +import failproofai_sdk + +# asyncio: nothing to do +async with failproofai_sdk.session(): + await asyncio.gather(worker(1), worker(2)) # both see the session + +# threads: wrap the callable +pool.submit(failproofai_sdk.propagate(work), x) +pool.map(failproofai_sdk.propagate(work), items) +threading.Thread(target=failproofai_sdk.propagate(work)).start() +loop.run_in_executor(None, failproofai_sdk.propagate(work), x) +``` + +Without `propagate()`, the worker's events raise a `TypeError` naming the fix, +rather than silently landing on no session. + +`propagate()` snapshots identity *values*, not the `Context` object — a +`Context` cannot be entered by two threads at once, so the obvious +`copy_context().run` form crashes on any reuse such as `pool.map`. + +--- + +## Instrumenting an unsupported framework + +Most agent frameworks give you three seams. Map them and you have a complete +trace. + +**1. Bracket the run** — wherever your framework starts and finishes a unit of +work: + +```python +with failproofai_sdk.session(): + with failproofai_sdk.agent(agent_name, goal=task): + result = framework.run(task) +``` + +**2. Bracket each tool** — in whatever the framework calls a tool wrapper or +middleware: + +```python +with failproofai_sdk.tool_call(name, input=args) as call: + call.output = original(**args) +``` + +**3. Pair each model call** — around the provider call: + +```python +failproofai_sdk.event.model_request(model=model, messages=messages) +reply = provider.complete(...) +failproofai_sdk.event.model_response( + model=model, response=text, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, +) +``` + +If the framework has a node, step or middleware boundary worth seeing, add +`hook_triggered` / `hook_completed` around it. Do **not** promote it to a nested +`agent()`. + +### If the framework has a global callback surface + +If it does expose a process-wide registration point, four rules from the shipped +adapters are worth copying: + +- **Never raise into the caller.** Wrap every callback so a bug in yours cannot + affect the instrumented call. +- **Do not hold a contextvar token between two callbacks.** When start and end + are separate calls, `ContextVar.reset(token)` raises across asyncio tasks and + threads alike. Keep a map from the framework's own run id to identity, and pass + `session_id=` / `agent_id=` explicitly. +- **Bound every map.** Orphaned starts are normal — a crashed run, an unconsumed + stream, a skipped end callback. Unbounded, that is a leak in a long-lived + server. +- **Restore exactly what you replaced.** Save the original attribute object; if + it is no longer yours at teardown, leave it alone rather than deleting somebody + else's patch. + +--- + +## Why there is no AutoGen adapter + +Two reasons, and both are decisions rather than backlog items. + +**`autogen-core` is discontinued.** The `autogen-core` / `autogen-agentchat` +line has been unmaintained since 2025-09-30. An adapter against a dead callback +surface is a dependency that cannot be fixed when it drifts. + +**AG2 has no global auto-instrument hook.** AG2 — the community fork — exposes +no process-wide registration point equivalent to LangChain's configure hook, +CrewAI's event bus, LlamaIndex's dispatcher or Pydantic AI's capability list. +Instrumenting it means wrapping each agent at each construction site, which is a +wrapper you paste into your own code — precisely the ergonomics the adapters +exist to remove. + +If AutoGen matters to you, the three seams above record the same fifteen event +types at the same fidelity. It just costs you the call sites. + +--- + +## Pitfalls + +### Spans that never finish + +**Symptom:** the dashboard shows a run still going, hours later. + +**Cause:** an opening event with no closing one — a `model_request` with no +`model_response`, or a `tool_use` with no `tool_result`. + +**Fix:** use the scopes, which guarantee the pair even when the body raises. If +you call `event.*` by hand, use `try` / `finally`. + +--- + +### `ValueError: duration_ms is auto-computed` + +**Symptom:** passing `duration_ms` to a closing event raises. + +**Cause:** it is measured by the SDK from the matching opening event, so passing +it is rejected on `tool_result`, `hook_completed`, `agent_resume` and +`human_input`. + +**Fix:** do not pass it. The exception is `model_response`, where it **is** +accepted, because only you know the real provider latency — and it must be an +`int`, since a float silently nulls the column server-side. + +--- + +### `TypeError: ... missing session_id` + +**Symptom:** a `TypeError` naming `session_id` from inside a worker. + +**Cause:** you emitted from a thread that never inherited the context. + +**Fix:** `failproofai_sdk.propagate()`. See [Threads and async](#threads-and-async). + +This is deliberately a loud error rather than a silent drop: an event with no +session is skipped by ingest and answered `200`, which is the exact silent +failure the identity layer exists to prevent. + +--- + +### Extra fields silently disappearing + +**Symptom:** you passed `model="x"` as an extra field and it did not show up, or +it overwrote something. + +**Cause:** extras are merged **last**, so an extra named like a declared field — +`tool_name`, `model`, `outcome`, `input_tokens` — would overwrite the real field +and change a promoted column. + +**Fix:** namespace yours. The adapters use an `fw_` prefix: + +```python +failproofai_sdk.event.tool_use( + tool_name="search", tool_call_id="c1", + my_region="eu-west-1", # fine + # model="x", # dropped, with a warning +) +``` + +--- + +### `agent_id` full of UUIDs + +**Symptom:** the agent filter has thousands of entries. + +**Cause:** `agent_id` is a low-cardinality facet column, and you put a run id in +it. + +**Fix:** use a role or node name. Put the real id in a payload field. + +--- + +## Runnable examples + +| file | what it shows | events | +|---|---|---| +| [`examples/quickstart.py`](examples/quickstart.py) | the three scopes, ~40 lines | 6 | +| [`examples/research_agent.py`](examples/research_agent.py) | a real OpenAI tool-calling loop, hand-instrumented | 14 | + +```bash +pip install failproofai-sdk openai +export OPENAI_API_KEY=sk-... +export FPAI_MODEL=gpt-4o-mini +python docs/manual/examples/quickstart.py +``` + +Both were run against a live model before shipping. diff --git a/sdk/python/docs/manual/examples/quickstart.py b/sdk/python/docs/manual/examples/quickstart.py new file mode 100644 index 000000000..98679e968 --- /dev/null +++ b/sdk/python/docs/manual/examples/quickstart.py @@ -0,0 +1,57 @@ +"""no framework — the scopes and `event.*` directly. + + pip install failproofai-sdk + python docs/manual/examples/quickstart.py + +There is no adapter here and nothing to instrument. This is the path for an +agent you wrote yourself, or a framework failproof does not support yet. + +Three scopes do the identity work: + + session() binds a session id. emits nothing. + agent() brackets a span with agent_start / agent_end. + tool_call() brackets a tool with tool_use / tool_result. + +Everything inside them can omit `session_id=` and `agent_id=` — the scopes bind +them on contextvars and every `event.*` call reads them back. +""" +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace + +failproofai_sdk.configure(environment="examples") + + +def look_up(city: str) -> str: + return {"tokyo": "37M", "delhi": "33M"}.get(city.lower(), "unknown") + + +def main() -> None: + banner("manual quickstart", "no framework · scopes + event.*") + + with failproofai_sdk.session() as sid: + with failproofai_sdk.agent("main", goal="answer one question"): + # tool_call() emits tool_use on entry and tool_result on exit, + # measuring duration and recording an exception as an error. + with failproofai_sdk.tool_call("population", input={"city": "tokyo"}) as call: + call.output = look_up("tokyo") + + failproofai_sdk.event.model_request(model=model(), messages=[ + {"role": "user", "content": "population of tokyo?"} + ]) + failproofai_sdk.event.model_response( + model=model(), output_tokens=3, duration_ms=210, response="37M" + ) + + trace(sid, title="manual quickstart") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/docs/manual/examples/research_agent.py b/sdk/python/docs/manual/examples/research_agent.py new file mode 100644 index 000000000..dcdb4afcb --- /dev/null +++ b/sdk/python/docs/manual/examples/research_agent.py @@ -0,0 +1,133 @@ +"""no framework — a real agent loop, hand-instrumented. + + pip install failproofai-sdk openai + python docs/manual/examples/research_agent.py + +A working tool-calling loop against the openai api with no agent framework at +all, instrumented by hand. This is the reference for "my agent is bespoke". + +Every event an adapter would emit for you appears here explicitly, so you can +see exactly what the adapters are doing on your behalf: + + session() -> identity only, no event + agent() -> agent_start / agent_end + event.model_request/response -> one pair per llm turn, with usage + tool_call() -> tool_use / tool_result, duration measured + event.error -> anything you want on the errors surface + +The one rule: emit the pairs. `model_request` without `model_response` is an +open span the dashboard renders as still-running forever. +""" +import json +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from openai import OpenAI + +failproofai_sdk.configure(environment="examples") + +client = OpenAI() + +_PRICE = {"widget": 42.0, "gadget": 17.5} +_STOCK = {"widget": 120, "gadget": 0} + +TOOLS = [ + { + "type": "function", + "function": { + "name": "price_of", + "description": "Unit price of an item. Valid: widget, gadget.", + "parameters": { + "type": "object", + "properties": {"item": {"type": "string"}}, + "required": ["item"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "stock_of", + "description": "Units in stock. Valid: widget, gadget.", + "parameters": { + "type": "object", + "properties": {"item": {"type": "string"}}, + "required": ["item"], + }, + }, + }, +] + + +def run_tool(name: str, args: dict) -> str: + if name == "price_of": + return str(_PRICE[args["item"].lower().strip()]) + if name == "stock_of": + return str(_STOCK[args["item"].lower().strip()]) + raise LookupError(f"unknown tool {name!r}") + + +def turn(messages: list) -> object: + """One llm call, bracketed by the model_request/model_response pair.""" + failproofai_sdk.event.model_request( + model=model(), + messages=[{"role": m.get("role"), "content": str(m.get("content"))[:200]} + for m in messages], + ) + reply = client.chat.completions.create( + model=model(), messages=messages, tools=TOOLS + ) + usage = reply.usage + failproofai_sdk.event.model_response( + model=model(), + response=reply.choices[0].message.content or "", + input_tokens=getattr(usage, "prompt_tokens", None), + output_tokens=getattr(usage, "completion_tokens", None), + ) + return reply.choices[0].message + + +def main() -> None: + banner("manual research agent", "no framework · hand-instrumented loop") + + messages = [ + {"role": "system", "content": "Use the tools for every number. Be terse."}, + {"role": "user", "content": "Price and stock for widget and gadget?"}, + ] + + with failproofai_sdk.session() as sid: + with failproofai_sdk.agent("inventory", goal="price and stock report"): + for _ in range(4): # bounded: an unbounded agent loop is its own bug + message = turn(messages) + calls = message.tool_calls or [] + if not calls: + print(" answer:", (message.content or "").strip()[:200], "\n") + break + + messages.append(message.model_dump(exclude_none=True)) + for call in calls: + args = json.loads(call.function.arguments or "{}") + # tool_call() emits tool_use now and tool_result on exit, + # turning a raised exception into `error` on the result. + with failproofai_sdk.tool_call( + call.function.name, tool_call_id=call.id, input=args + ) as handle: + handle.output = run_tool(call.function.name, args) + messages.append({ + "role": "tool", + "tool_call_id": call.id, + "content": str(handle.output), + }) + + trace(sid, title="manual research agent") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/docs/pydantic_ai/README.md b/sdk/python/docs/pydantic_ai/README.md new file mode 100644 index 000000000..b9d9d162e --- /dev/null +++ b/sdk/python/docs/pydantic_ai/README.md @@ -0,0 +1,323 @@ +# Pydantic AI + +- [Install](#install) +- [The integration](#the-integration) +- [Read this first: construction order](#read-this-first-construction-order) +- [How it attaches](#how-it-attaches) +- [What gets recorded](#what-gets-recorded) +- [A complete example](#a-complete-example) +- [Options](#options) +- [Errors, retries and control flow](#errors-retries-and-control-flow) +- [Pitfalls](#pitfalls) +- [Runnable examples](#runnable-examples) + +--- + +## Install + +```bash +pip install 'failproofai-sdk[pydantic-ai]' # pins pydantic-ai-slim >=2.0,<3 +``` + +**Supported:** `pydantic-ai-slim` 2.0 → 3.0. 2.0 is a capability floor: it is the +release that removed `Agent(instrument=...)` and introduced `AbstractCapability`, +which is the entire surface this adapter is built on. There is no way to +instrument 1.x with this adapter. + +--- + +## The integration + +```python +import failproofai_sdk +from pydantic_ai import Agent + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() # <- BEFORE constructing any Agent + +agent = Agent("openai:gpt-4o-mini", system_prompt="Be terse.") + +with failproofai_sdk.session(): + result = agent.run_sync("...") +``` + +--- + +## Read this first: construction order + +**`instrument()` must run before you construct an `Agent`.** + +The capability is appended at construction time. An `Agent` built before +`instrument()` ran carries no capability and records **nothing** — and there is +no error, because nothing went wrong. This is the single most common way to get +an empty trace with this adapter. + +This bites hardest at module scope: + +```python +# agents.py +from pydantic_ai import Agent +agent = Agent("openai:gpt-4o-mini") # constructed at import time + + +# main.py +import agents # <- agent built HERE, uninstrumented +import failproofai_sdk +failproofai_sdk.instrument() # too late for agents.agent +``` + +Fix it by instrumenting first: + +```python +# main.py +import failproofai_sdk +failproofai_sdk.instrument() + +import agents # now the agent gets the capability +``` + +Or construct agents inside a function rather than at import time. + +Agents built **while** instrumented keep the capability object, so +`uninstrument()` flips a flag the capability reads rather than trying to reach +back into agents it no longer owns. That means you can safely uninstrument and +re-instrument without rebuilding your agents. + +--- + +## How it attaches + +`instrument()` wraps `Agent.__init__` so that every agent constructed afterwards +gets a `FailproofAI` capability appended to its capability list. + +The capability implements the middleware protocol Pydantic AI 2.0 introduced, so +it sees: + +- run start and end, +- each model request and response, with usage, +- each tool call and its result. + +It composes with your own capabilities — it is appended, not substituted, and it +declares an ordering so it wraps the outside of the stack rather than +interfering with middleware you added. + +--- + +## What gets recorded + +| Pydantic AI | Failproof event | When | +|---|---|---| +| agent run | `agent_start` / `agent_end` | `run` / `run_sync` / `run_stream` | +| model request | `model_request` / `model_response` | each model call, with `usage` | +| tool call | `tool_use` / `tool_result` | each tool call, with the args the model sent | +| `ModelRetry` from a tool | `tool_result` with `error` | the tool asked for a retry | +| unhandled exception | `error` + `agent_end(outcome="failed")` | the run raises | + +There is **no** `hook_triggered` / `hook_completed` pair here, and no +human-in-the-loop pair. That is not a gap — Pydantic AI has no node or step +boundary to bracket, and no built-in human pause. If you build either, emit the +events yourself; see [`../manual/`](../manual/). + +`output_type` makes no difference to the trace. A typed run and a string run +produce the same events. + +--- + +## A complete example + +```python +import failproofai_sdk +from pydantic import BaseModel +from pydantic_ai import Agent, ModelRetry + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() + +PRICE = {"widget": 42.0, "gadget": 17.5} +STOCK = {"widget": 120, "gadget": 0} + + +class Report(BaseModel): + """What the run must produce. Validated by pydantic_ai, not by us.""" + + headline: str + out_of_stock: list[str] + + +agent = Agent( + "openai:gpt-4o-mini", + output_type=Report, + system_prompt="Use the tools for every number. If a tool fails, note it and continue.", +) + + +@agent.tool_plain +def price_of(item: str) -> float: + """Unit price of an item. Valid: widget, gadget.""" + return PRICE[item.lower().strip()] + + +@agent.tool_plain +def stock_of(item: str) -> int: + """Units in stock. Valid: widget, gadget.""" + return STOCK[item.lower().strip()] + + +@agent.tool_plain +def restock_eta(item: str) -> str: + """Restock ETA. Not available for anything.""" + # ModelRetry hands the failure back to the model instead of raising out of + # run_sync. Recorded as an errored tool_result either way. + raise ModelRetry(f"no restock schedule for {item!r} — answer without it") + + +with failproofai_sdk.session(): + with failproofai_sdk.agent("inventory", goal="stock report"): + result = agent.run_sync( + "For 'widget' and 'gadget', get price and stock. " + "For anything out of stock, try the restock ETA. Then produce the report." + ) + +print(result.output.headline) +print(result.output.out_of_stock) +``` + +Produces **20 events across 6 types**: + +``` + 1 +0.000s agent_start inventory + 2 +0.005s agent_start agent · under inventory + 3 +0.006s model_request gpt-5.6-terra + 4 +4.036s model_response gpt-5.6-terra · 90 out-tok + 5 +4.039s tool_use price_of + ... + 15 +7.028s tool_use restock_eta + 16 +7.029s tool_result restock_eta · error + 17 +7.032s model_request gpt-5.6-terra + 18 +10.411s model_response gpt-5.6-terra · 101 out-tok + 19 +10.416s agent_end agent · success + 20 +10.417s agent_end inventory · success +``` + +Note events 16–18: the tool failed, the model was told, it made another call and +recovered. The run ends `success` and the failure is still on the record. + +The nested `agent` span at depth 2 is Pydantic AI's own run, sitting inside the +`inventory` scope you opened. That is where the model and tool events hang. + +--- + +## Options + +```python +failproofai_sdk.instrument( + "pydantic_ai", + session_id=None, # pin every run to one session id + capture_content=True, # False drops prompts and completions from payloads +) +``` + +--- + +## Errors, retries and control flow + +Pydantic AI raises exceptions for three genuinely different things, and the +adapter distinguishes them: + +| exception | treated as | result | +|---|---|---| +| `ModelRetry`, `ToolRetryError`, `ToolFailedError` | **a real tool failure** | `tool_result` with `error`; run can still end `success` | +| `SkipToolExecution`, `SkipToolValidation`, `SkipModelRequest`, `CallDeferred`, `ApprovalRequired` | **control flow** | not an error; the run is being steered | +| anything else | **a failure** | `error` + `agent_end(outcome="failed")` | + +`ModelRetry` is deliberately in the first group, not the second. It means an +attempt genuinely failed and the model was asked to try again — which is exactly +what a tool span's `error` field is for. Classifying it as control flow would +hide real tool failures behind a green run. + +The control-flow list is resolved by name at import time and tolerates every one +of them being absent: a minor release renaming one degrades to "treat it as an +error" rather than raising an `AttributeError` inside your run. + +--- + +## Pitfalls + +### An empty trace, no errors anywhere + +**Symptom:** the run works, no warnings, and no events. + +**Cause:** the `Agent` was constructed before `instrument()` ran. See +[construction order](#read-this-first-construction-order). + +**Check it:** + +```python +agent = Agent("openai:gpt-4o-mini") +print([type(c).__name__ for c in agent.root_capability.capabilities]) +# ['FailproofAI', 'ToolSearch', 'PendingMessageDrainCapability'] +``` + +Pydantic AI merges the list you pass into one `root_capability`, so there is no +`agent.capabilities` attribute — reading that raises `AttributeError`. + +--- + +### A plain exception in a tool kills the run + +**Symptom:** `run_sync` raises instead of the model working around a failed tool. + +**Cause:** a bare `raise` propagates. That is Pydantic AI's design. + +**Fix:** raise `ModelRetry` with a message the model can act on. + +```python +@agent.tool_plain +def restock_eta(item: str) -> str: + """Restock ETA.""" + raise ModelRetry(f"no schedule for {item!r} — answer without it") +``` + +The failure is recorded as an errored `tool_result` either way; this only +decides whether the run survives it. + +--- + +### There is a nested agent span I did not create + +**Symptom:** wrapping in `failproofai_sdk.agent("inventory")` gives you +`inventory` **and** a child called `agent`. + +**Cause:** that child is Pydantic AI's own run span, and it is where the model +and tool events hang. It is correct. + +**Fix:** if you want one span instead of two, drop your own scope and let the +framework's span be the root — you lose the custom name. + +--- + +### Tracebacks look truncated at the top + +**Symptom:** the `traceback` field starts with `[older frames truncated]…`. + +**Cause:** Pydantic AI's async graph stack is comfortably longer than the 8KB +field limit. A traceback's last line is the exception itself, so this one field +is trimmed from the **front**, not the back — keeping the head like every other +field would ship 8KB of framework frames and drop the one line anybody reads. + +--- + +## Runnable examples + +| file | what it shows | events | +|---|---|---| +| [`examples/quickstart.py`](examples/quickstart.py) | one tool, one turn | 8 | +| [`examples/research_agent.py`](examples/research_agent.py) | 3 tools, typed output, one tool that retries | 20 | + +```bash +export OPENAI_API_KEY=sk-... +export FPAI_MODEL=gpt-4o-mini +python docs/pydantic_ai/examples/quickstart.py +``` + +Both were run against a live model before shipping. diff --git a/sdk/python/docs/pydantic_ai/examples/quickstart.py b/sdk/python/docs/pydantic_ai/examples/quickstart.py new file mode 100644 index 000000000..f94a5b31c --- /dev/null +++ b/sdk/python/docs/pydantic_ai/examples/quickstart.py @@ -0,0 +1,48 @@ +"""pydantic_ai — the smallest thing that produces a trace. + + pip install 'failproofai-sdk[pydantic-ai]' + python docs/pydantic_ai/examples/quickstart.py + +`instrument()` appends a capability to every `Agent` constructed afterwards, so +runs, tools and model calls are captured without touching the call sites. + +Order matters here and nowhere else: an `Agent` built BEFORE `instrument()` does +not carry the capability. Construct agents after instrumenting, or at import +time in a module imported after it. +""" +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from pydantic_ai import Agent + +failproofai_sdk.configure(environment="examples") +failproofai_sdk.instrument() + +agent = Agent(f"openai:{model()}", system_prompt="Be terse.") + + +@agent.tool_plain +def population(city: str) -> str: + """Population of a city.""" + return {"tokyo": "37M", "delhi": "33M"}.get(city.lower(), "unknown") + + +def main() -> None: + banner("pydantic_ai quickstart", "one tool, one turn") + + with failproofai_sdk.session() as sid: + result = agent.run_sync("Population of Tokyo? Use the tool.") + + print(" answer:", str(result.output).strip()[:120]) + trace(sid, title="pydantic_ai quickstart") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/docs/pydantic_ai/examples/research_agent.py b/sdk/python/docs/pydantic_ai/examples/research_agent.py new file mode 100644 index 000000000..b1e14a8fe --- /dev/null +++ b/sdk/python/docs/pydantic_ai/examples/research_agent.py @@ -0,0 +1,94 @@ +"""pydantic_ai — a typed multi-tool run, plus a tool that raises. + + pip install 'failproofai-sdk[pydantic-ai]' + python docs/pydantic_ai/examples/research_agent.py + +What this demonstrates that the quickstart does not: + +* three tools over several turns, each a `tool_use`/`tool_result` pair carrying + the arguments the model actually sent; +* a tool that raises `ModelRetry`, recorded as `tool_result` with `error` set + while the run still finishes `success` — the adapter deliberately does NOT + treat a retry as control flow, because an attempt really did fail and that is + what a tool span's `error` field is for; +* a typed `output_type`, so the run ends with a validated object and the trace + still reads the same — instrumentation does not care about your result type; +* `usage` on `model_response`, which is where per-run token spend comes from. +""" +import sys +from pathlib import Path + +# Only so this file can import the shared trace printer from docs/_shared/. +# Delete these two lines and the `_shared` import below and the example still +# instruments correctly — it just stops printing its own trace at the end. +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +import failproofai_sdk +from _shared import banner, model, trace +from pydantic import BaseModel +from pydantic_ai import Agent, ModelRetry + +failproofai_sdk.configure(environment="examples") +failproofai_sdk.instrument() + +_PRICE = {"widget": 42.0, "gadget": 17.5} +_STOCK = {"widget": 120, "gadget": 0} + + +class Report(BaseModel): + """What the run must produce. Validated by pydantic_ai, not by us.""" + + headline: str + out_of_stock: list[str] + + +agent = Agent( + f"openai:{model()}", + output_type=Report, + system_prompt=( + "Use the tools for every number. If a tool fails, note it and continue." + ), +) + + +@agent.tool_plain +def price_of(item: str) -> float: + """Unit price of an item. Valid: widget, gadget.""" + return _PRICE[item.lower().strip()] + + +@agent.tool_plain +def stock_of(item: str) -> int: + """Units in stock. Valid: widget, gadget.""" + return _STOCK[item.lower().strip()] + + +@agent.tool_plain +def restock_eta(item: str) -> str: + """Restock ETA. Not available for anything.""" + # `ModelRetry` is how pydantic_ai hands a failure back to the model instead + # of raising out of `run_sync`. The failproof trace records it as an errored + # tool_result either way; this just lets the run finish so you can watch the + # model work around it. + raise ModelRetry(f"no restock schedule for {item!r} — answer without it") + + +def main() -> None: + banner("pydantic_ai research agent", "3 tools · typed output · one failing call") + + prompt = ( + "For 'widget' and 'gadget', get price and stock. For anything out of " + "stock, try the restock ETA. Then produce the report." + ) + + with failproofai_sdk.session() as sid: + with failproofai_sdk.agent("inventory", goal="stock report"): + result = agent.run_sync(prompt) + + print(" headline: ", result.output.headline[:120]) + print(" out of stock: ", result.output.out_of_stock, "\n") + trace(sid, title="pydantic_ai research agent") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/failproofai_sdk/__init__.py b/sdk/python/failproofai_sdk/__init__.py index 073640e20..6e6bc3b30 100644 --- a/sdk/python/failproofai_sdk/__init__.py +++ b/sdk/python/failproofai_sdk/__init__.py @@ -1,11 +1,44 @@ +"""Telemetry for AI agents: emit events, spool them, let the daemon ship them. + +Three surfaces, in the order most people meet them: + +* **Scopes** — `session()`, `agent()`, `tool_call()`. Context managers that bind + run identity and, for the latter two, bracket a run with its own events. Work + under `with` and `async with`. +* **Adapters** — `instrument()`. Auto-detects LangChain/LangGraph, CrewAI, + LlamaIndex and Pydantic AI in the process and wires them to the scopes above. +* **`event.*`** — the 15 event methods, for anything the adapters do not cover. + +`session_id` and `agent_id` are optional on every event method: omitted, they +resolve from the enclosing scope. Nothing bound and nothing passed is an error, +never a silent drop — ingest skips an event with no session and answers 200. +""" + +from typing import Any + from failproofai_sdk._version import __version__ from failproofai_sdk._environment import set_environment -from failproofai_sdk._events import EventNamespace from failproofai_sdk._resolver import set_base_dir -from failproofai_sdk._writer import EventWriter, _validated_interval +from failproofai_sdk._context import Identity, current, propagate +from failproofai_sdk._runtime import event +from failproofai_sdk._scopes import agent, session, tool_call +from failproofai_sdk._writer import _validated_interval +from failproofai_sdk import _runtime -_writer = EventWriter() -event = EventNamespace(_writer) +__all__ = [ + "__version__", + "configure", + "event", + "session", + "agent", + "tool_call", + "current", + "Identity", + "propagate", + "instrument", + "uninstrument", + "_writer", +] def configure( @@ -41,5 +74,41 @@ def configure( """ flush_interval = _validated_interval(flush_interval) set_base_dir(base_dir) - _writer.set_flush_interval(flush_interval) + _runtime.writer.set_flush_interval(flush_interval) set_environment(environment) + + +def instrument(framework: str | None = None, **options: Any): + """Install the framework adapters. + + With no argument, auto-detects the frameworks already imported in this + process. Pass a name (`"langchain"`, `"crewai"`, `"llama_index"`, + `"pydantic_ai"`) to install exactly one. + + The import is inside the function on purpose: `failproofai_sdk.integrations` + reaches for framework packages, and `import failproofai_sdk` is + contractually zero-dependency — a promise `tests/test_zero_dependencies.py` + enforces both by scanning the core modules and by launching a fresh + interpreter to prove no framework lands in `sys.modules`. + """ + from failproofai_sdk.integrations import instrument as _impl + + return _impl(framework, **options) + + +def uninstrument(framework: str | None = None): + """Reverse `instrument()`, restoring the original attributes. + + Lazy-imported for the same reason as `instrument()`. + """ + from failproofai_sdk.integrations import uninstrument as _impl + + return _impl(framework) + + +# MUST be last: any `import failproofai_sdk.` binds the *module* onto this +# package as `failproofai_sdk._writer`. Rebinding it here to the instance is what +# keeps the published `failproofai_sdk._writer.flush_now()` recipe working — and +# is why a test reaching for the MODULE has to go through +# `sys.modules["failproofai_sdk._writer"]`. +_writer = _runtime.writer diff --git a/sdk/python/failproofai_sdk/_context.py b/sdk/python/failproofai_sdk/_context.py new file mode 100644 index 000000000..de70135dc --- /dev/null +++ b/sdk/python/failproofai_sdk/_context.py @@ -0,0 +1,166 @@ +"""Ambient run identity, carried on contextvars. + +Before this module the SDK had no ambient session: every `event.*` call took +`session_id` and `agent_id` as required keyword arguments and nothing propagated +them. Threading both through every function that might emit an event is what makes +instrumentation sprawl into a diff nobody wants to review. + +Two contextvars carry it instead. Read them through `current()`, or let +`failproofai_sdk._events` fall back to them when a caller omits the identity. + +Why a tuple and not a list +-------------------------- +`_AGENT_STACK` holds a **tuple**. A `ContextVar[list]` is shared *by reference* +across tasks and threads, so `.append()` in one task mutates the value every other +task sees — which is exactly the cross-run event mixing contextvars are here to +prevent, wearing a contextvars costume. It passes every single-threaded test. +Push is `set(stack + (aid,))`; pop is `reset(token)`. + +There is deliberately no `_AGENT_ID` var: the top of the stack *is* the current +agent id, so the two cannot drift apart, and `parent_id` is `stack[-2]`. +""" + +import contextvars +import functools +import logging +from dataclasses import dataclass +from typing import Any, Callable + +logger = logging.getLogger(__name__) + +# The agent_id used when events are emitted with a session bound but no agent +# scope. "main" is the convention the skill and the reference integration already +# teach, so an un-scoped event lands somewhere sensible rather than raising. +DEFAULT_AGENT_ID = "main" + +_SESSION_ID: contextvars.ContextVar[str | None] = contextvars.ContextVar( + "failproofai_sdk_session_id", default=None +) +_AGENT_STACK: contextvars.ContextVar[tuple[str, ...]] = contextvars.ContextVar( + "failproofai_sdk_agent_stack", default=() +) + + +@dataclass(frozen=True, slots=True) +class Identity: + """The run identity in scope. Never None — check `session_id is None` instead.""" + + session_id: str | None + agent_id: str | None + parent_id: str | None + depth: int + + +def current() -> Identity: + """The identity bound to the current context. + + `failproofai_sdk.current().session_id is None` means nothing is bound — either no + scope was entered, or this is a fresh thread that did not inherit one (see + `propagate`). + """ + stack = _AGENT_STACK.get() + return Identity( + session_id=_SESSION_ID.get(), + agent_id=stack[-1] if stack else None, + parent_id=stack[-2] if len(stack) >= 2 else None, + depth=len(stack), + ) + + +def session_id() -> str | None: + """The bound session id, or None. Hot path — allocates no Identity.""" + return _SESSION_ID.get() + + +def agent_id() -> str: + """The current agent id, falling back to DEFAULT_AGENT_ID.""" + stack = _AGENT_STACK.get() + return stack[-1] if stack else DEFAULT_AGENT_ID + + +def parent_agent_id() -> str | None: + """The enclosing agent id, or None at depth 0 or 1.""" + stack = _AGENT_STACK.get() + return stack[-2] if len(stack) >= 2 else None + + +def bind_session(sid: str) -> contextvars.Token: + return _SESSION_ID.set(sid) + + +def push_agent(aid: str) -> contextvars.Token: + return _AGENT_STACK.set(_AGENT_STACK.get() + (aid,)) + + +def reset(token: contextvars.Token | None) -> None: + """Restore a contextvar to its pre-`set` value, tolerating a cross-context token. + + `ContextVar.reset()` raises `ValueError: Token was created in a different + Context` when the token was minted in another thread *or another asyncio task*. + That happens when a scope is entered in one task and exited in another, which is + the caller's bug — but an observability library's correct response is a debug + line, not an exception raised on top of whatever they were already doing. + """ + if token is None: + return + try: + token.var.reset(token) + except ValueError: + logger.debug( + "failproofai_sdk: context token reset across contexts; identity left as-is", + exc_info=True, + ) + + +Snapshot = tuple[str | None, tuple[str, ...]] + + +def snapshot() -> Snapshot: + """Capture the identity *values* currently bound.""" + return (_SESSION_ID.get(), _AGENT_STACK.get()) + + +def restore(snap: Snapshot) -> tuple[contextvars.Token, contextvars.Token]: + """Bind a snapshot into the calling context.""" + sid, stack = snap + return (_SESSION_ID.set(sid), _AGENT_STACK.set(stack)) + + +def discard(tokens: tuple[contextvars.Token, contextvars.Token] | None) -> None: + if tokens is None: + return + for token in reversed(tokens): + reset(token) + + +def propagate(fn: Callable[..., Any]) -> Callable[..., Any]: + """Wrap `fn` so it runs with the identity bound *right now*. + + pool.submit(failproofai_sdk.propagate(work), x) + pool.map(failproofai_sdk.propagate(work), items) + threading.Thread(target=failproofai_sdk.propagate(work)).start() + loop.run_in_executor(None, failproofai_sdk.propagate(work), x) + + contextvars propagate into asyncio tasks automatically but **not into new + threads** — a thread starts with an empty context, so without this every event + a worker emits is dropped (or, since this change, raises TypeError). + + This deliberately snapshots *values* rather than doing + `functools.partial(contextvars.copy_context().run, fn)`. A `Context` object + cannot be entered by two threads at once (`RuntimeError: cannot enter context: + ... is already entered`), so the copy_context form crashes the caller's worker + on any reuse — `pool.map`, a retried submit. Mutations made inside `ctx.run` + also persist in that Context, so a reused one leaks the previous call's agent + stack into the next. + """ + snap = snapshot() + + @functools.wraps(fn) + def _failproofai_propagated(*args: Any, **kwargs: Any) -> Any: + tokens = restore(snap) + try: + return fn(*args, **kwargs) + finally: + discard(tokens) + + return _failproofai_propagated diff --git a/sdk/python/failproofai_sdk/_environment.py b/sdk/python/failproofai_sdk/_environment.py index cea07d89f..ce01148e8 100644 --- a/sdk/python/failproofai_sdk/_environment.py +++ b/sdk/python/failproofai_sdk/_environment.py @@ -1,14 +1,63 @@ +import logging + _DEFAULT_ENVIRONMENT = "dev" _environment: str | None = None +logger = logging.getLogger("failproofai_sdk") + + +def _reject_comma(env: str, source: str) -> None: + """A comma in `environment` makes ingest skip EVERY event carrying it. + + The endpoint splits this field on commas to build its filter facets, so a + line whose `environment` contains one is discarded — the whole line, not the + field. It answers 200 with `{"accepted":0,"skipped":N}`, the daemon deletes + the delivered batch, and the run that produced it is simply never in the + dashboard: no exception here, nothing in the agent's output, and an empty + session list that looks exactly like an agent nobody ran. + + `failproofaid` already refuses a comma in `collector.environment` for this + reason (`crates/fpai-collect/src/config.rs`). The SDK is the other writer of + the same field and did not, so `AGENTEYE_ENVIRONMENT="prod,eu"` — a wholly + reasonable thing to type — silently threw away everything the process + emitted. + """ + if "," in env: + raise ValueError( + f"environment must not contain a comma (got {env!r} from {source}). " + "The ingest endpoint skips every event whose environment has one, so " + "this would silently discard all telemetry from this process. Use a " + "single label, e.g. 'prod-eu'." + ) + def get_environment() -> str: if _environment is not None: return _environment import os - return os.environ.get("AGENTEYE_ENVIRONMENT") or _DEFAULT_ENVIRONMENT + + raw = os.environ.get("AGENTEYE_ENVIRONMENT") + if not raw: + return _DEFAULT_ENVIRONMENT + if "," in raw: + # Raising here would blow up inside `to_dict()` on an arbitrary event, + # far from the thing that set it, and take the caller's agent down with + # it — a telemetry library must not do that. Warn once and fall back to + # a label ingest will actually accept, so the events land under a + # visibly-wrong environment instead of vanishing. + logger.warning( + "failproofai_sdk: AGENTEYE_ENVIRONMENT=%r contains a comma, which makes " + "the ingest endpoint skip every event carrying it. Falling back to %r. " + "Use a single label, e.g. 'prod-eu'.", + raw, + _DEFAULT_ENVIRONMENT, + ) + return _DEFAULT_ENVIRONMENT + return raw def set_environment(env: str | None) -> None: global _environment + if env: + _reject_comma(env, "configure(environment=...)") _environment = env if env else None diff --git a/sdk/python/failproofai_sdk/_events.py b/sdk/python/failproofai_sdk/_events.py index a899b2a1c..8ce2229a2 100644 --- a/sdk/python/failproofai_sdk/_events.py +++ b/sdk/python/failproofai_sdk/_events.py @@ -2,6 +2,8 @@ from datetime import datetime, timezone from typing import Any +from failproofai_sdk import _context + from failproofai_sdk._schema import ( AgentEndEvent, AgentPauseEvent, @@ -90,7 +92,11 @@ def _validate_identity(name: str, value) -> None: under one blank id, so the data looks present and is silently merged. """ if not isinstance(value, str): - raise ValueError( + # TypeError, not ValueError: the wrong TYPE was supplied, and this is + # also what a caller got before identity became optional — omitting a + # required keyword argument has always been a TypeError, so code that + # catches one keeps working. + raise TypeError( f"{name} must be a str — the server skips any event whose {name} is " f"not a JSON string, and answers 200 as though it stored it. " f"Got {type(value).__name__}: {value!r}" @@ -101,6 +107,52 @@ def _validate_identity(name: str, value) -> None: f"sent this way is silently grouped under one blank id." ) +def _resolve_identity(session_id, agent_id) -> "tuple[str, str]": + """Fill an omitted `session_id`/`agent_id` from the ambient scope, then validate. + + Both arguments were required on all 15 methods, which meant threading them + through every function that might emit — the diff nobody wants to review, and + the reason the reference integration shipped a contextvars wrapper as markdown + for customers to paste in. `session()` / `agent()` bind them instead. + + ORDER MATTERS. The validation runs on the RESOLVED value, not the argument. + Validating first would reject every ambient call; resolving without validating + would put the silent-skip back: ingest drops an event whose `session_id` is not + a JSON string and answers `200 OK` with `{"accepted":0,"skipped":1}`, so a run + with nothing bound would vanish rather than fail. + + Called AFTER `_validate_fields`, deliberately. A reserved `**field` is a + fault in the call itself and reads identically from anywhere, so reporting it + first gives a stable, reproducible message; the identity error depends on + where the call was made from, and is the less useful of the two to hear when + both are true. + + `agent_id` falls back to `DEFAULT_AGENT_ID` rather than raising — an event + emitted inside `session()` with no `agent()` around it lands somewhere sensible, + which is the convention the skill already teaches. `session_id` has no such + default: inventing one would scatter a run across as many sessions as it has + emit sites. + """ + if session_id is None: + session_id = _context.session_id() + if agent_id is None: + agent_id = _context.agent_id() # DEFAULT_AGENT_ID when no agent scope + + if session_id is None: + # TypeError for the same reason: this is a missing required argument, + # which is what it literally was until the scopes made it optional. + raise TypeError( + "session_id is required and nothing is bound. Pass session_id=..., or " + "wrap the call in `with failproofai_sdk.session():` / " + "`with failproofai_sdk.agent(\"name\"):`. A new thread does not inherit " + "the ambient scope — hand work to it with " + "`failproofai_sdk.propagate(fn)`." + ) + _validate_identity("session_id", session_id) + _validate_identity("agent_id", agent_id) + return session_id, agent_id + + def _measured_duration_ms(start_ts, end_ts) -> "int | None": """Whole milliseconds between a paired start and end, or None. @@ -149,34 +201,40 @@ def _measured_duration_ms(start_ts, end_ts) -> "int | None": _PENDING_CAP = 10_000 -# Every pairing in `_pending` is namespaced by what it pairs AND by whose it is. -# Neither half was always there, and each missing half produced the same class of -# wrong answer: a duration measured between two unrelated events. +# Every pairing in `_pending` is keyed by what it pairs and by the SESSION it +# belongs to — and deliberately NOT by the agent. +# +# The rule: include what makes the id unique, exclude what can legitimately +# change between the start event and the end event. +# +# * KIND belongs in the key. Tool pairs keyed on the bare `tool_call_id` and hook +# pairs on the bare `hook_id` shared one flat keyspace, so a caller whose tool +# call and hook happened to share an id — not exotic, both are frequently the +# harness's own step id — got a `hook_completed` that consumed the `tool_use` +# timestamp, and then a `tool_result` with no duration at all. # -# The kind prefix came first. Tool pairs keyed on the bare `tool_call_id` and -# hook pairs on the bare `hook_id`, sharing one flat keyspace, so a caller whose -# tool call and hook happened to share an id — not exotic, both are frequently -# the harness's own step id — got a `hook_completed` that consumed the -# `tool_use` timestamp, and then a `tool_result` with no duration at all. +# * SESSION belongs in the key. `_pending` lives on one process-wide namespace, +# so two sessions in one process — a supervisor running agents concurrently, +# the ordinary multi-agent shape — collided on any shared step id. Starting +# `step-1` in session A and then in session B overwrote A's timestamp; A's +# result reported B's interval and B's reported none. # -# The session/agent half was still missing afterwards, and only from these two: -# human and pause pairs had it from the start. `_pending` lives on one -# process-wide `EventNamespace`, so two sessions in one process — a supervisor -# running agents concurrently, the ordinary multi-agent shape — collided on any -# shared step id. Starting `step-1` in session A then in session B overwrote A's -# timestamp; A's result then reported B's interval, and B's result reported none. -# Both are plausible numbers, neither is an error, and nothing downstream can -# tell which sessions were affected. +# * AGENT DOES NOT. This was the tempting third component and it is wrong. Once a +# framework runs tools inside sub-agents, a `tool_use` opened under `planner` +# and closed under `worker` is routine — LangGraph and CrewAI both do it — and +# an agent-scoped key makes those pairs miss entirely, silently dropping +# `duration_ms` for exactly the nested runs that most need it. A session cannot +# change under a pair; an agent can. # # These are correlation keys only; they are never emitted and never leave the -# process, so namespacing them changes no wire format. It only changes -# `duration_ms` in the colliding case, from a fabricated value to a correct one. -def _tool_key(session_id: str, agent_id: str, tool_call_id: str) -> str: - return f"tool:{session_id}:{agent_id}:{tool_call_id}" +# process, so the shape changes no wire format. It only changes `duration_ms` in +# the colliding cases, from a fabricated or missing value to a correct one. +def _tool_key(session_id: str, tool_call_id: str) -> str: + return f"tool:{session_id}:{tool_call_id}" -def _hook_key(session_id: str, agent_id: str, hook_id: str) -> str: - return f"hook:{session_id}:{agent_id}:{hook_id}" +def _hook_key(session_id: str, hook_id: str) -> str: + return f"hook:{session_id}:{hook_id}" class EventNamespace: @@ -235,18 +293,17 @@ def _fmt_ts(dt: datetime) -> str: def tool_use( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, tool_name: str, tool_call_id: str, input: dict | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() - self._track_pending(_tool_key(session_id, agent_id, tool_call_id), ts) + self._track_pending(_tool_key(session_id, tool_call_id), ts) self._writer.submit( ToolUseEvent( timestamp=self._fmt_ts(ts), @@ -262,8 +319,8 @@ def tool_use( def tool_result( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, tool_name: str, tool_call_id: str, output: Any | None = None, @@ -272,11 +329,10 @@ def tool_result( ) -> None: if "duration_ms" in fields: raise ValueError("duration_ms is auto-computed by the SDK and cannot be passed by the caller") - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() - start_ts = self._pending.pop(_tool_key(session_id, agent_id, tool_call_id), None) + start_ts = self._pending.pop(_tool_key(session_id, tool_call_id), None) duration_ms = _measured_duration_ms(start_ts, ts) self._writer.submit( ToolResultEvent( @@ -295,17 +351,17 @@ def tool_result( def model_request( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, model: str | None = None, messages: list[dict] | None = None, system: Any | None = None, tools: list[dict] | None = None, + request_id: str | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() self._writer.submit( ModelRequestEvent( @@ -316,6 +372,7 @@ def model_request( messages=messages, system=system, tools=tools, + request_id=request_id, extra_fields=fields, ).to_dict() ) @@ -323,14 +380,15 @@ def model_request( def model_response( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, model: str | None = None, stop_reason: str | None = None, input_tokens: int | None = None, output_tokens: int | None = None, content: Any | None = None, role: str | None = None, + request_id: str | None = None, **fields, ) -> None: # Named parameters, so they never reach `_validate_fields`. They are also @@ -338,9 +396,8 @@ def model_response( # provider's usage object gets whatever that object holds. _validate_promoted_numeric("input_tokens", input_tokens) _validate_promoted_numeric("output_tokens", output_tokens) - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() self._writer.submit( ModelResponseEvent( @@ -353,6 +410,7 @@ def model_response( output_tokens=output_tokens, content=content, role=role, + request_id=request_id, extra_fields=fields, ).to_dict() ) @@ -360,15 +418,14 @@ def model_response( def agent_start( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, goal: str | None = None, parent_id: str | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() self._writer.submit( AgentStartEvent( @@ -384,15 +441,14 @@ def agent_start( def agent_end( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, outcome: str | None = None, summary: str | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() self._writer.submit( AgentEndEvent( @@ -408,18 +464,17 @@ def agent_end( def agent_pause( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, pause_id: str, reason: str | None = None, user_id: str | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() - self._track_pending(f"pause:{session_id}:{agent_id}:{pause_id}", ts) + self._track_pending(f"pause:{session_id}:{pause_id}", ts) self._writer.submit( AgentPauseEvent( timestamp=self._fmt_ts(ts), @@ -435,8 +490,8 @@ def agent_pause( def agent_resume( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, pause_id: str, reason: str | None = None, user_id: str | None = None, @@ -444,11 +499,10 @@ def agent_resume( ) -> None: if "duration_ms" in fields: raise ValueError("duration_ms is auto-computed by the SDK and cannot be passed by the caller") - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() - start_ts = self._pending.pop(f"pause:{session_id}:{agent_id}:{pause_id}", None) + start_ts = self._pending.pop(f"pause:{session_id}:{pause_id}", None) duration_ms = _measured_duration_ms(start_ts, ts) self._writer.submit( AgentResumeEvent( @@ -466,19 +520,18 @@ def agent_resume( def hook_triggered( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, hook_name: str, hook_id: str, trigger_event: str | None = None, input: Any | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() - self._track_pending(_hook_key(session_id, agent_id, hook_id), ts) + self._track_pending(_hook_key(session_id, hook_id), ts) self._writer.submit( HookTriggeredEvent( timestamp=self._fmt_ts(ts), @@ -495,8 +548,8 @@ def hook_triggered( def hook_completed( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, hook_name: str, hook_id: str, outcome: str | None = None, @@ -506,11 +559,10 @@ def hook_completed( ) -> None: if "duration_ms" in fields: raise ValueError("duration_ms is auto-computed by the SDK and cannot be passed by the caller") - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() - start_ts = self._pending.pop(_hook_key(session_id, agent_id, hook_id), None) + start_ts = self._pending.pop(_hook_key(session_id, hook_id), None) duration_ms = _measured_duration_ms(start_ts, ts) self._writer.submit( HookCompletedEvent( @@ -530,16 +582,15 @@ def hook_completed( def error( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, error_type: str, message: str, traceback: str | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() self._writer.submit( ErrorEvent( @@ -556,19 +607,18 @@ def error( def human_wait( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, input_id: str, prompt: str | None = None, options: list[str] | None = None, reason: str | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() - self._track_pending(f"human:{session_id}:{agent_id}:{input_id}", ts) + self._track_pending(f"human:{session_id}:{input_id}", ts) self._writer.submit( HumanWaitEvent( timestamp=self._fmt_ts(ts), @@ -585,19 +635,18 @@ def human_wait( def human_input( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, input_id: str, response: str | None = None, **fields, ) -> None: if "duration_ms" in fields: raise ValueError("duration_ms is auto-computed by the SDK and cannot be passed by the caller") - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() - start_ts = self._pending.pop(f"human:{session_id}:{agent_id}:{input_id}", None) + start_ts = self._pending.pop(f"human:{session_id}:{input_id}", None) duration_ms = _measured_duration_ms(start_ts, ts) self._writer.submit( HumanInputEvent( @@ -614,15 +663,14 @@ def human_input( def human_pause( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, reason: str | None = None, user_id: str | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() self._writer.submit( HumanPauseEvent( @@ -638,16 +686,15 @@ def human_pause( def human_interrupt( self, *, - session_id: str, - agent_id: str, + session_id: str | None = None, + agent_id: str | None = None, reason: str | None = None, user_id: str | None = None, at_step: str | None = None, **fields, ) -> None: - _validate_identity("session_id", session_id) - _validate_identity("agent_id", agent_id) self._validate_fields(fields) + session_id, agent_id = _resolve_identity(session_id, agent_id) ts = self._now() self._writer.submit( HumanInterruptEvent( diff --git a/sdk/python/failproofai_sdk/_runtime.py b/sdk/python/failproofai_sdk/_runtime.py new file mode 100644 index 000000000..b606ae7ce --- /dev/null +++ b/sdk/python/failproofai_sdk/_runtime.py @@ -0,0 +1,21 @@ +"""The process-wide writer and event namespace. + +These used to be constructed in `failproofai_sdk/__init__.py`. They live in a +leaf module now so that `_scopes` and the framework adapters can reach the +namespace without importing the `failproofai_sdk` package itself, which would be +a circular import. + +Constructing `EventWriter` starts a daemon thread and registers the module-level +atexit flush, and that still happens at `import failproofai_sdk` time — +`__init__.py` imports this module, so the timing is unchanged. + +Reach the namespace as `_runtime.event`, an attribute lookup at call time rather +than a `from ... import event` binding, so a test can swap in a recording +namespace and the scopes pick it up. +""" + +from failproofai_sdk._events import EventNamespace +from failproofai_sdk._writer import EventWriter + +writer = EventWriter() +event = EventNamespace(writer) diff --git a/sdk/python/failproofai_sdk/_schema.py b/sdk/python/failproofai_sdk/_schema.py index 48ea96042..c16d9ba9a 100644 --- a/sdk/python/failproofai_sdk/_schema.py +++ b/sdk/python/failproofai_sdk/_schema.py @@ -64,13 +64,18 @@ class ModelRequestEvent: messages: list[dict] | None = None system: Any | None = None tools: list[dict] | None = None + #: Pairs this request with its response. Appended LAST in `_build`'s ordered + #: list so an event that omits it serialises byte-for-byte as before — + #: `test_wire_format.py` freezes those bytes, and the dedup key hashes them. + request_id: str | None = None extra_fields: dict = field(default_factory=dict) def to_dict(self) -> dict: return _build( {"timestamp": self.timestamp, "session_id": self.session_id, "agent_id": self.agent_id, "type": "model_request"}, [("model", self.model), ("messages", self.messages), - ("system", self.system), ("tools", self.tools)], + ("system", self.system), ("tools", self.tools), + ("request_id", self.request_id)], self.extra_fields, ) @@ -86,6 +91,8 @@ class ModelResponseEvent: output_tokens: int | None = None content: Any | None = None role: str | None = None + #: The `request_id` of the `model_request` this answers. See above. + request_id: str | None = None extra_fields: dict = field(default_factory=dict) def to_dict(self) -> dict: @@ -93,7 +100,8 @@ def to_dict(self) -> dict: {"timestamp": self.timestamp, "session_id": self.session_id, "agent_id": self.agent_id, "type": "model_response"}, [("model", self.model), ("stop_reason", self.stop_reason), ("input_tokens", self.input_tokens), ("output_tokens", self.output_tokens), - ("content", self.content), ("role", self.role)], + ("content", self.content), ("role", self.role), + ("request_id", self.request_id)], self.extra_fields, ) diff --git a/sdk/python/failproofai_sdk/_scopes.py b/sdk/python/failproofai_sdk/_scopes.py new file mode 100644 index 000000000..e710a4268 --- /dev/null +++ b/sdk/python/failproofai_sdk/_scopes.py @@ -0,0 +1,364 @@ +"""Context-manager scopes: `session()`, `agent()`, `tool_call()`. + +These are the ergonomic surface over `failproofai_sdk.event.*`. Each one binds run +identity onto the contextvars in `failproofai_sdk._context` so that everything emitted +inside the block — including code that has never heard of the SDK's identity +arguments — lands on the right session and agent. + +Why classes and not `@contextlib.contextmanager` +------------------------------------------------ +A `@contextmanager` generator supports `with` only. An agent framework is +half-async, so the same scope has to work under `async with` too, and +`@asynccontextmanager` would mean a second, separately-maintained copy of the +body. These are plain classes with `__enter__`/`__exit__` **and** +`__aenter__`/`__aexit__`, where the async pair delegates to the sync pair. No +scope awaits anything — `writer.submit()` is a `deque.append` — so the delegation +is not a lie, and `async with` provably produces byte-identical events. + +The namespace is reached as `_runtime.event` (attribute lookup at call time, not +a `from ... import event` binding) both to avoid importing `failproofai_sdk` from inside +`failproofai_sdk` and so tests can swap in a recording namespace. +""" + +import contextvars +import sys +import traceback as _traceback +import uuid +from typing import Any, Literal + +from failproofai_sdk import _context +from failproofai_sdk import _runtime + + +class _Auto: + """Sentinel for `agent(parent_id=...)`. + + `parent_id` has three states and `None` is a meaningful one of them, so the + default cannot be `None`: + + * `AUTO` — infer the enclosing agent from the context stack (the default); + * `None` — force a root span, emitting no `parent_id` at all; + * `"str"` — use this id verbatim. + """ + + __slots__ = () + + def __repr__(self) -> str: # pragma: no cover - debugging aid + return "AUTO" + + +AUTO = _Auto() + + +def _is_cancellation(exc_type: type) -> bool: + """True for `asyncio.CancelledError` / `GeneratorExit`. + + A cancellation is not a failure: it must not emit an `error` event, or every + cancelled run pollutes the Errors surface. `asyncio` is looked up through + `sys.modules` rather than imported, so `import failproofai_sdk` stays cheap — if + `asyncio` was never imported, nothing in the process can have raised its + `CancelledError`. + """ + if issubclass(exc_type, GeneratorExit): + return True + asyncio = sys.modules.get("asyncio") + return asyncio is not None and issubclass(exc_type, asyncio.CancelledError) + + +def _describe(exc_type: type, exc: BaseException | None) -> str: + text = str(exc) if exc is not None else "" + return f"{exc_type.__name__}: {text}" if text else exc_type.__name__ + + +class session: + """Bind a session id (and optionally an agent id) for the enclosing block. + + with failproofai_sdk.session() as sid: + failproofai_sdk.event.agent_start(agent_id="main", goal="...") + + Emits **no events** — it is identity only. `agent()` is what brackets a run + with `agent_start`/`agent_end`. + + `session_id=None` reuses an already-bound session if there is one, and + otherwise generates `uuid4().hex`. That inheritance is what lets a nested + scope stay inside one run instead of splitting it into two sessions. + """ + + __slots__ = ("_requested", "_agent_id", "id", "_sid_token", "_agent_token") + + def __init__(self, session_id: str | None = None, *, agent_id: str | None = None) -> None: + self._requested = session_id + self._agent_id = agent_id + self.id: str | None = None + self._sid_token: "contextvars.Token | None" = None + self._agent_token: "contextvars.Token | None" = None + + def _enter(self) -> str: + sid = self._requested or _context.session_id() or uuid.uuid4().hex + self.id = sid + self._sid_token = _context.bind_session(sid) + if self._agent_id is not None: + self._agent_token = _context.push_agent(self._agent_id) + return sid + + def _exit(self) -> None: + # Unwind in reverse, and unconditionally: a scope that leaks an agent + # frame misattributes every later event in the process. + _context.reset(self._agent_token) + _context.reset(self._sid_token) + self._agent_token = None + self._sid_token = None + + def __enter__(self) -> str: + return self._enter() + + # `Literal[False]`, not `bool`: a `__exit__` typed `bool` tells every caller's + # type checker this scope MIGHT swallow the exception. It never does. + def __exit__(self, exc_type, exc, tb) -> Literal[False]: + self._exit() + return False + + async def __aenter__(self) -> str: + return self._enter() + + async def __aexit__(self, exc_type, exc, tb) -> Literal[False]: + self._exit() + return False + + +class agent: + """Bracket a run (or a sub-run) with `agent_start` / `agent_end`. + + with failproofai_sdk.agent("planner", goal=question): + ... + + `agent_id` is positional on purpose. It is the one argument integrators type + on every single call site, and the rest of the SDK's keyword-only discipline + exists to stop *identity* arguments being passed by accident — which is not a + risk here. + + Keep `agent_id` low-cardinality (a node/role name, never a UUID): it is a + `LowCardinality(String)` column and the primary facet across every session. + + `**fields` are attached to `agent_start` only; `agent_end` carries `outcome` + and `summary`. + + Exit semantics, which are the whole point of the class: + + | exception | events | outcome | + |---------------------------------|-------------------|-------------| + | none | `agent_end` | `outcome=` | + | `Exception` | `error`, then end | `"failed"` | + | `KeyboardInterrupt`/`SystemExit`| `error`, then end | `"failed"` | + | `CancelledError`/`GeneratorExit`| `agent_end` only | `"cancelled"`| + + `error` strictly *before* `agent_end`, because the dashboard closes the agent + span at `agent_end` and anything after it is attributed to nothing. The + literal is `"failed"`, never `"failure"` — only `error|failed|timeout|rejected` + count as a failure server-side. The exception is always re-raised. + """ + + __slots__ = ( + "agent_id", + "_requested_sid", + "_goal", + "_parent_id", + "_outcome", + "_summary", + "_fields", + "session_id", + "_sid_token", + "_agent_token", + ) + + def __init__( + self, + agent_id: str = "main", + *, + session_id: str | None = None, + goal: str | None = None, + parent_id: "str | None | _Auto" = AUTO, + outcome: str | None = "success", + summary: str | None = None, + **fields: Any, + ) -> None: + self.agent_id = agent_id + self._requested_sid = session_id + self._goal = goal + self._parent_id = parent_id + self._outcome = outcome + self._summary = summary + self._fields = fields + self.session_id: str | None = None + self._sid_token: "contextvars.Token | None" = None + self._agent_token: "contextvars.Token | None" = None + + def _enter(self) -> _context.Identity: + sid = self._requested_sid or _context.session_id() or uuid.uuid4().hex + self.session_id = sid + + if isinstance(self._parent_id, _Auto): + parent = _context.current().agent_id # None when nothing is open + else: + parent = self._parent_id + + self._sid_token = _context.bind_session(sid) + self._agent_token = _context.push_agent(self.agent_id) + try: + _runtime.event.agent_start( + session_id=sid, + agent_id=self.agent_id, + goal=self._goal, + parent_id=parent, + **self._fields, + ) + except BaseException: + # A rejected agent_start (a reserved **field, say) must not leave a + # half-entered scope behind: `__exit__` never runs if `__enter__` + # raises. + self._unwind() + raise + return _context.current() + + def _unwind(self) -> None: + _context.reset(self._agent_token) + _context.reset(self._sid_token) + self._agent_token = None + self._sid_token = None + + def _exit(self, exc_type, exc, tb) -> bool: + try: + if exc_type is None: + outcome = self._outcome + elif _is_cancellation(exc_type): + outcome = "cancelled" + else: + outcome = "failed" + _runtime.event.error( + session_id=self.session_id, + agent_id=self.agent_id, + error_type=exc_type.__name__, + message=str(exc) if exc is not None else "", + traceback="".join(_traceback.format_exception(exc_type, exc, tb)), + ) + _runtime.event.agent_end( + session_id=self.session_id, + agent_id=self.agent_id, + outcome=outcome, + summary=self._summary, + ) + finally: + # In `finally` so the stack is intact even if emission itself blew + # up. A leaked frame is worse than a lost event. + self._unwind() + return False + + def __enter__(self) -> _context.Identity: + return self._enter() + + def __exit__(self, exc_type, exc, tb) -> bool: + return self._exit(exc_type, exc, tb) + + async def __aenter__(self) -> _context.Identity: + return self._enter() + + async def __aexit__(self, exc_type, exc, tb) -> bool: + return self._exit(exc_type, exc, tb) + + +class ToolCall: + """The handle `tool_call()` yields. Set `.output`; read `.id`.""" + + __slots__ = ("_id", "output") + + def __init__(self, tool_call_id: str) -> None: + self._id = tool_call_id + self.output: Any = None + + @property + def id(self) -> str: + """The correlation id on both the `tool_use` and the `tool_result`.""" + return self._id + + def __repr__(self) -> str: # pragma: no cover - debugging aid + return f"ToolCall(id={self._id!r})" + + +class tool_call: + """Bracket a tool invocation with `tool_use` / `tool_result`. + + with failproofai_sdk.tool_call("web_search", input={"q": q}) as t: + t.output = search(q) + + `tool_call_id` defaults to `uuid4().hex`. Identity comes from the enclosing + scope; if nothing is bound, the underlying `event.tool_use()` raises the + usual `TypeError` naming the fix. + + On failure this emits `tool_result(error="TypeName: msg")` and **no `error` + event**. A tool failure the agent loop catches is not a run-level error, and + one that propagates is reported exactly once, by the enclosing `agent()`. + Cancellation closes the leaf with no `error` string at all, for the same + reason `agent()` does not mark it failed. + """ + + __slots__ = ("tool_name", "_requested_id", "_input", "_fields", "_box", "_sid", "_aid") + + def __init__( + self, + tool_name: str, + *, + tool_call_id: str | None = None, + input: dict | None = None, + **fields: Any, + ) -> None: + self.tool_name = tool_name + self._requested_id = tool_call_id + self._input = input + self._fields = fields + self._box: ToolCall | None = None + self._sid: str | None = None + self._aid: str | None = None + + def _enter(self) -> ToolCall: + # Resolve once, at entry: a tool that pushes its own scope inside must + # not make the closing tool_result land on a different agent_id. + self._sid = _context.session_id() + self._aid = _context.agent_id() + box = ToolCall(self._requested_id or uuid.uuid4().hex) + self._box = box + _runtime.event.tool_use( + session_id=self._sid, + agent_id=self._aid, + tool_name=self.tool_name, + tool_call_id=box.id, + input=self._input, + **self._fields, + ) + return box + + def _exit(self, exc_type, exc, tb) -> bool: + assert self._box is not None + error = None + if exc_type is not None and not _is_cancellation(exc_type): + error = _describe(exc_type, exc) + _runtime.event.tool_result( + session_id=self._sid, + agent_id=self._aid, + tool_name=self.tool_name, + tool_call_id=self._box.id, + output=self._box.output, + error=error, + ) + return False + + def __enter__(self) -> ToolCall: + return self._enter() + + def __exit__(self, exc_type, exc, tb) -> bool: + return self._exit(exc_type, exc, tb) + + async def __aenter__(self) -> ToolCall: + return self._enter() + + async def __aexit__(self, exc_type, exc, tb) -> bool: + return self._exit(exc_type, exc, tb) diff --git a/sdk/python/failproofai_sdk/integrations/__init__.py b/sdk/python/failproofai_sdk/integrations/__init__.py new file mode 100644 index 000000000..db18f4253 --- /dev/null +++ b/sdk/python/failproofai_sdk/integrations/__init__.py @@ -0,0 +1,240 @@ +"""Framework adapters: the registry behind `failproofai_sdk.instrument()`. + + import failproofai_sdk + failproofai_sdk.instrument() # every framework already imported + failproofai_sdk.instrument("crewai") # exactly one + failproofai_sdk.uninstrument() # put everything back + +**This module is stdlib only, and imports no adapter until asked.** The registry +maps a name to a dotted module path *as a string*; `importlib.import_module` +runs on demand. That is what keeps `import failproofai_sdk` free of LangChain. + +Auto-detection reads `sys.modules`, deliberately **not** +`importlib.util.find_spec`. `find_spec` would report "installed", and acting on +that means importing a framework the user is not using — 200ms and a pile of +transitive imports charged to a library that was supposed to be invisible. If +the framework is not imported, there is nothing to instrument. + +Writing an adapter +------------------ +A module registered here must expose a module-level object named `adapter` +implementing `failproofai_sdk.integrations._core.Adapter`: + + name: str # the registry name + module: str # the framework module auto-detect looks for in sys.modules + def install(**options) -> None + def uninstall() -> None + +`install()` must save the **original attribute object** it replaces — use +`_core.Patcher`, which also does the "somebody patched on top of us" check — and +`uninstall()` must restore that saved object. Never re-import to restore: that +hands back whatever the current value happens to be, which is how two +instrumentation libraries silently un-patch each other. + +Every callback the adapter hands to the framework goes through `_core.safe`, and +every event goes through a `_core.RunTracker`. Adapters own the translation +table and nothing else. + +The four names are registered here **now**, before their modules exist, so that +adding an adapter is one new file rather than an edit to this one. +""" + +import importlib +import logging +import sys +import threading +from typing import Any + +from failproofai_sdk.integrations import _compat, _core +from failproofai_sdk.integrations._core import Adapter + +logger = logging.getLogger("failproofai_sdk.integrations") + +__all__ = ["instrument", "uninstrument", "active", "available"] + +# name -> dotted module path. A string, imported on demand. +_REGISTRY: dict[str, str] = { + "langchain": "failproofai_sdk.integrations.langchain", + "crewai": "failproofai_sdk.integrations.crewai", + "llama_index": "failproofai_sdk.integrations.llama_index", + "pydantic_ai": "failproofai_sdk.integrations.pydantic_ai", +} + +# Spellings people actually type. LangGraph is served by the LangChain adapter +# because LangGraph runs on langchain-core's callback manager. +_ALIASES: dict[str, str] = { + "langgraph": "langchain", + "langchain_core": "langchain", + "llamaindex": "llama_index", + "llama-index": "llama_index", + "pydantic-ai": "pydantic_ai", + "pydanticai": "pydantic_ai", +} + +# name -> the framework modules whose presence in sys.modules means "this +# framework is in use". Kept here rather than read off `adapter.module` so that +# detection imports nothing at all, not even our own adapter module. +_DETECT: dict[str, tuple[str, ...]] = { + "langchain": ("langchain_core", "langchain", "langgraph"), + "crewai": ("crewai",), + "llama_index": ("llama_index", "llama_index.core"), + "pydantic_ai": ("pydantic_ai",), +} + +# Guards _ACTIVE and every install/uninstall. `instrument()` is called from +# application startup, which in a web server can be several threads at once. +_LOCK = threading.Lock() +_ACTIVE: dict[str, Adapter] = {} + + +def available() -> tuple[str, ...]: + """Every registry name that can be instrumented, aliases excluded.""" + return tuple(sorted(_REGISTRY)) + + +def active() -> tuple[str, ...]: + """Names currently instrumented.""" + with _LOCK: + return tuple(sorted(_ACTIVE)) + + +def _canonical(name: str) -> str: + key = str(name).strip().lower().replace(" ", "") + key = _ALIASES.get(key, key) + if key not in _REGISTRY: + valid = ", ".join(sorted(set(_REGISTRY) | set(_ALIASES))) + raise ValueError( + f"failproofai_sdk: unknown framework {name!r}. Valid names are: {valid}. " + f"(Call failproofai_sdk.instrument() with no argument to auto-detect.)" + ) + return key + + +def _detected() -> list[str]: + """Registry names whose framework is already imported in this process.""" + found = [] + for name in _REGISTRY: + modules = _DETECT.get(name, (name,)) + if any(module in sys.modules for module in modules): + found.append(name) + return found + + +def _load(name: str) -> Adapter: + module = importlib.import_module(_REGISTRY[name]) + adapter = getattr(module, "adapter", None) + if adapter is None: + # A module that is itself the adapter is fine too; the attribute is the + # convention, not the contract. + adapter = module + for attribute in ("install", "uninstall"): + if not callable(getattr(adapter, attribute, None)): + raise TypeError( + f"failproofai_sdk: adapter {_REGISTRY[name]!r} does not implement " + f"{attribute}() — see failproofai_sdk.integrations._core.Adapter." + ) + return adapter # type: ignore[return-value] + + +def instrument(framework: str | None = None, **options: Any) -> tuple[str, ...]: + """Install the adapters. Returns the names newly instrumented. + + With no argument, instruments every framework already imported in this + process. Instrumenting something already active is a no-op that returns + `()`, so calling this from two code paths (or from a reloading dev server) + cannot double-record. + + An unknown name raises `ValueError` listing the valid ones — a typo that + silently records nothing is the worst outcome available. An adapter whose + `install()` raises is logged and skipped; the others still install, because + a broken LlamaIndex should not cost you LangGraph. `FAILPROOFAI_SDK_STRICT=1` turns + that skip back into a raise. + """ + if framework is None: + names = _detected() + if not names: + # WARNING, not debug. This fires only when somebody explicitly asked + # for instrumentation and got none — the import-order mistake of + # calling instrument() above the `import langchain` line — and the + # result is a process that records nothing at all, with the adapter + # installed and the docs followed. At debug level the message that + # names the exact fix was invisible under every default logging + # config, which made the one mistake that costs you all your + # telemetry the one mistake we said nothing about. + logger.warning( + "failproofai_sdk: instrument() found no supported framework in sys.modules, " + "so NOTHING was instrumented. Import your framework before calling " + "instrument(), or name one explicitly: %s.", + # The names this call would have accepted, rather than one + # hardcoded example. A reader who is not using CrewAI has to + # work out for themselves whether the message is a suggestion + # or a diagnosis, which is a poor use of the one line they get. + ", ".join(f"instrument({name!r})" for name in sorted(_REGISTRY)), + ) + else: + names = [_canonical(framework)] + + installed: list[str] = [] + with _LOCK: + for name in names: + if name in _ACTIVE: + continue + try: + adapter = _load(name) + adapter.install(**options) + except Exception: + if _core.strict(): + raise + logger.warning( + "failproofai_sdk: could not instrument %r; the rest of your process is " + "unaffected and other adapters still installed. Set " + "FAILPROOFAI_SDK_STRICT=1 to raise instead.", + name, + exc_info=True, + ) + continue + _ACTIVE[name] = adapter + installed.append(name) + return tuple(installed) + + +def uninstrument(framework: str | None = None) -> tuple[str, ...]: + """Reverse `instrument()`. Returns the names removed. Never raises. + + With no argument, removes everything. An unknown name, or a name that was + never instrumented, is a no-op — teardown that can fail is teardown people + stop calling. + """ + if framework is None: + with _LOCK: + names = list(_ACTIVE) + else: + try: + names = [_canonical(framework)] + except ValueError as exc: + logger.warning("%s", exc) + return () + + removed: list[str] = [] + with _LOCK: + for name in names: + adapter = _ACTIVE.pop(name, None) + if adapter is None: + continue + try: + adapter.uninstall() + except Exception: + logger.warning( + "failproofai_sdk: %r did not uninstall cleanly; it is no longer " + "registered, but some patches may remain.", + name, + exc_info=True, + ) + removed.append(name) + if not _ACTIVE: + # Nothing is instrumented any more, so a later instrument() starts + # from a clean slate rather than inheriting a degraded call site or + # a warning that has "already been shown". + _core.reset_failures() + _compat.reset_warnings() + return tuple(removed) diff --git a/sdk/python/failproofai_sdk/integrations/_compat.py b/sdk/python/failproofai_sdk/integrations/_compat.py new file mode 100644 index 000000000..a9b82a197 --- /dev/null +++ b/sdk/python/failproofai_sdk/integrations/_compat.py @@ -0,0 +1,247 @@ +"""Version and capability probes for the framework adapters. + +Extras (`failproofai_sdk[langchain]`) express a *floor*, not enforcement: most users +already have the framework and will never install an extra. So the real check +happens here, at `instrument()` time, in three tiers: + +1. **framework not importable** -> a hard `ImportError` whose message contains + the literal install command. Instrumenting is an explicit user action, so + silently doing nothing is never the right answer. +2. **importable but outside the declared range** -> `FailproofAICompatWarning`, + once, then best-effort. A ceiling exists because without one a clean build a + year from now pulls the next major, the callback API shifts, and the adapter + stops receiving events *while raising nothing*. +3. **a capability probe fails** -> warn and no-op **that hook only**, never the + whole adapter. + +`FAILPROOFAI_SDK_STRICT_INTEGRATIONS=1` promotes every warning here to an exception. +Warn-by-default is only defensible because there is a supported way to make it +fail loudly. + +Why the version comparison is naive +----------------------------------- +`import failproofai_sdk` is contractually zero-dependency, so this cannot use +`packaging`. `parse_version` therefore reads the **leading numeric components +only** and stops at the first component that is not purely numeric: + + "1.5.2" -> (1, 5, 2) + "2.0.0b1" -> (2, 0, 0) # pre-release suffix ignored + "0.14.23.post1" -> (0, 14, 23) # local/post segments ignored + "1.2.dev0" -> (1, 2) + +That means `2.0.0b1` compares **equal** to `2.0.0`, so a pre-release of a major +we have declared a ceiling against will not be flagged. That is deliberate: +the alternative is shipping a PEP 440 parser, and being wrong about a release +candidate is much cheaper than a runtime dependency. + +Versions are always read with `importlib.metadata.version(dist)` and **never** +`module.__version__` — that attribute is not guaranteed to exist and several of +the frameworks we target do not define it. +""" + +import importlib +import importlib.metadata +import logging +import os +import threading +import warnings +from types import ModuleType +from typing import Any, Callable + +logger = logging.getLogger("failproofai_sdk.integrations") + +__all__ = [ + "FailproofAICompatWarning", + "parse_version", + "version_string", + "version_tuple", + "require_module", + "check_version", + "probe", + "warn", + "strict_integrations", + "set_strict_integrations", + "reset_warnings", +] + + +class FailproofAICompatWarning(UserWarning): + """A framework is outside the range this adapter was written against.""" + + +_TRUTHY = frozenset({"1", "true", "yes", "on"}) + + +def _env_flag(name: str) -> bool: + """Read a boolean env var. Shared with `_core` so both flags parse alike.""" + return os.environ.get(name, "").strip().lower() in _TRUTHY + + +# Cached, because it is read on every warning and every `safe()` failure, and +# resettable, because a test that cannot flip the switch cannot test the policy. +# Same shape as `failproofai_sdk._environment`: a module global that overrides the env +# var, with None meaning "not decided yet, go look". +_strict_integrations: bool | None = None + + +def strict_integrations() -> bool: + global _strict_integrations + if _strict_integrations is None: + _strict_integrations = _env_flag("FAILPROOFAI_SDK_STRICT_INTEGRATIONS") + return _strict_integrations + + +def set_strict_integrations(value: bool | None) -> None: + """Override the flag. `None` re-reads `FAILPROOFAI_SDK_STRICT_INTEGRATIONS`.""" + global _strict_integrations + _strict_integrations = value + + +_warned: set[str] = set() +_warn_lock = threading.Lock() + + +def reset_warnings() -> None: + """Forget which warnings have already fired (tests; also `uninstrument()`).""" + with _warn_lock: + _warned.clear() + + +def warn(message: str, *, key: str | None = None) -> None: + """Warn once per `key`, or raise if strict. + + Deduplicated because these fire from `install()` *and* from hot callbacks: + a per-call warning on a chatty framework is its own outage. + """ + if strict_integrations(): + raise FailproofAICompatWarning(message) + dedup = key or message + with _warn_lock: + if dedup in _warned: + return + _warned.add(dedup) + warnings.warn(message, FailproofAICompatWarning, stacklevel=3) + logger.warning("%s", message) + + +def parse_version(text: str) -> tuple[int, ...]: + """Leading numeric components of a version string. See the module docstring.""" + parts: list[int] = [] + for chunk in str(text).split("."): + digits = "" + for ch in chunk: + if not ch.isdigit(): + break + digits += ch + if not digits: + break + parts.append(int(digits)) + if len(digits) != len(chunk): + # A partially numeric component ("0b1", "dev0", "post1") ends the + # numeric prefix — everything after it is a pre/post/local segment. + break + return tuple(parts) + + +def version_string(dist: str) -> str | None: + """The installed version of a distribution, or None if it is not installed. + + `dist` is the *distribution* name (`langchain-core`), which is frequently + not the module name (`langchain_core`). + """ + try: + return importlib.metadata.version(dist) + except importlib.metadata.PackageNotFoundError: + return None + except Exception: # pragma: no cover - a broken METADATA must not break us + logger.debug("failproofai_sdk: could not read version of %r", dist, exc_info=True) + return None + + +def version_tuple(dist: str) -> tuple[int, ...] | None: + text = version_string(dist) + return parse_version(text) if text else None + + +def require_module(module: str, *, dist: str, extra: str) -> ModuleType: + """Import a framework module or raise with the literal install command. + + Tier 1. `instrument("langchain")` on a machine without LangChain is a + mistake the user can fix in one command, so we hand them the command. + """ + try: + return importlib.import_module(module) + except ImportError as exc: + raise ImportError( + f"failproofai_sdk: cannot instrument {extra!r} because {module!r} is not importable. " + f"Install it with: pip install 'failproofai_sdk[{extra}]' " + f"(or install {dist} directly)." + ) from exc + + +def check_version( + framework: str, + dist: str, + *, + minimum: str | None = None, + below: str | None = None, + reason: str | None = None, +) -> bool: + """Tier 2. True when `dist` is inside [minimum, below); warns once if not. + + Returns True (best effort) for an unknown version too — a framework + installed from a git checkout has no usable metadata, and refusing to + instrument it would be a worse answer than trying. + """ + found = version_string(dist) + if found is None: + return True + got = parse_version(found) + if not got: + return True + + if minimum is not None and got < parse_version(minimum): + warn( + f"failproofai_sdk: {dist} {found} is older than the {minimum} this {framework} " + f"adapter was written against" + + (f" ({reason})" if reason else "") + + ". Instrumenting anyway; some events may be missing.", + key=f"{framework}:{dist}:min", + ) + return False + if below is not None and got >= parse_version(below): + warn( + f"failproofai_sdk: {dist} {found} is newer than the <{below} this {framework} " + f"adapter was written against. Instrumenting anyway, but a callback API " + f"change would make it stop recording silently — please report this.", + key=f"{framework}:{dist}:max", + ) + return False + return True + + +def probe(framework: str, hook: str, check: Callable[[], Any]) -> bool: + """Tier 3. Run a capability probe; on failure warn and disable ONE hook. + + if probe("langchain", "on_interrupt", lambda: langgraph.callbacks.GraphCallbackHandler): + ...wire it up... + + A missing capability is never a reason to abandon the whole adapter: the + other 90% of the events are still correct and still worth having. + """ + try: + ok = bool(check()) + except Exception as exc: + warn( + f"failproofai_sdk: {framework} capability probe for {hook!r} failed ({exc!r}); " + f"that hook is disabled, the rest of the adapter is unaffected.", + key=f"{framework}:{hook}", + ) + return False + if not ok: + warn( + f"failproofai_sdk: {framework} does not provide {hook!r} in this version; " + f"that hook is disabled, the rest of the adapter is unaffected.", + key=f"{framework}:{hook}", + ) + return ok diff --git a/sdk/python/failproofai_sdk/integrations/_core.py b/sdk/python/failproofai_sdk/integrations/_core.py new file mode 100644 index 000000000..7754915fd --- /dev/null +++ b/sdk/python/failproofai_sdk/integrations/_core.py @@ -0,0 +1,956 @@ +"""The parts every framework adapter shares: failure policy, patching, identity. + +An adapter under `failproofai_sdk/integrations/` is supposed to be a **translation +table** and nothing else. Everything that is genuinely hard — never raising into +the customer's call stack, restoring exactly what we replaced, mapping a +framework's run ids onto Failproof AI identity, keeping payloads inside the store's +patience — lives here, in one copy. If an adapter needs something added to this +module, that is a signal the core is wrong, not that the adapter is special. + +Three things in here are load-bearing and easy to "fix" into a bug: + +* `safe()` catches `Exception`, **never `BaseException`** — see the comment on + it before you change that. +* `RunTracker` never touches contextvars. `ContextVar.reset(token)` raises + across asyncio tasks as well as threads, so a callback surface whose start and + end are separate calls can never hold a token between them. +* `fw_fields()` is a safety rule, not a style rule. `_schema._build()` merges + extra fields **last**, so an extra named `tool_name` silently overwrites the + declared one and changes the promoted column. +""" + +import dataclasses +import functools +import logging +import re +import threading +import uuid +from collections.abc import Mapping, Sequence +from collections.abc import Set as AbcSet +from typing import Any, Callable, Protocol + +from failproofai_sdk import _context, _runtime, _schema +from failproofai_sdk._context import DEFAULT_AGENT_ID, Identity +from failproofai_sdk._events import _RESERVED +from failproofai_sdk._version import __version__ +from failproofai_sdk.integrations import _compat + +logger = logging.getLogger("failproofai_sdk.integrations") + +__all__ = [ + "Adapter", + "RunTracker", + "Patcher", + "safe", + "call_safely", + "wrap_callable", + "is_wrapped", + "unwrap", + "strict", + "set_strict", + "reset_failures", + "truncate", + "payload", + "fw_fields", + "guard_extras", + "framework_fields", + "normalize_agent_id", + "ms", + "FIELD_LIMIT", + "EVENT_BUDGET", + "FORBIDDEN_EXTRAS", +] + + +# --------------------------------------------------------------------------- +# The adapter protocol +# --------------------------------------------------------------------------- + +class Adapter(Protocol): + """What `failproofai_sdk/integrations/.py` must expose as `adapter`. + + `module` is the framework module that must already be in `sys.modules` for + auto-detection to pick this adapter up; the registry keeps its own copy of + that mapping so detection never has to import anything. + + `install()` must save the **original attribute object** it replaces (use + `Patcher`), and `uninstall()` must restore that saved object rather than + re-importing or reconstructing it. + """ + + name: str + module: str + + def install(self, **options: Any) -> None: + pass + + def uninstall(self) -> None: + pass + + +# --------------------------------------------------------------------------- +# Failure policy +# --------------------------------------------------------------------------- + +# Everything under `integrations/` obeys one rule: never raise into the +# customer's call stack. Observability that takes the process down with it is +# worse than no observability. FAILPROOFAI_SDK_STRICT=1 inverts that for tests and for +# debugging an adapter that has gone quiet — without it you can only ever prove +# "it didn't crash", never "it swallowed the right thing". +_strict: bool | None = None + + +def strict() -> bool: + global _strict + if _strict is None: + _strict = _compat._env_flag("FAILPROOFAI_SDK_STRICT") + return _strict + + +def set_strict(value: bool | None) -> None: + """Override the flag. `None` re-reads `FAILPROOFAI_SDK_STRICT`.""" + global _strict + _strict = value + + +# After this many failures at one call site we stop calling it. A broken adapter +# should cost one log line, not 40% of the process and a full disk. +_MAX_FAILURES = 3 + +_failures: dict[str, int] = {} +_disabled: set[str] = set() +_failure_lock = threading.Lock() + + +def reset_failures() -> None: + """Re-enable every degraded call site (tests; also `uninstrument()`).""" + with _failure_lock: + _failures.clear() + _disabled.clear() + + +def is_degraded(site: str) -> bool: + return site in _disabled + + +def _site_of(fn: Callable[..., Any]) -> str: + module = getattr(fn, "__module__", None) or "?" + qualname = getattr(fn, "__qualname__", None) or getattr(fn, "__name__", None) or repr(fn) + return f"{module}.{qualname}" + + +def call_safely(fn: Callable[..., Any], args: tuple, kwargs: dict, site: str) -> Any: + """Call `fn`, swallowing `Exception` and degrading a repeatedly failing site. + + Catches `Exception` and **not** `BaseException` on purpose. + `asyncio.CancelledError` has been a `BaseException` since Python 3.8, as is + `KeyboardInterrupt` and `SystemExit`; swallowing those would silently break + cancellation in every instrumented async application — the task gets + cancelled, our handler eats the CancelledError, and the framework carries on + running work that was supposed to stop. `GeneratorExit` is the same story + for generators. If you are here to "fix" this to `BaseException`, don't. + """ + if site in _disabled: + return None + try: + return fn(*args, **kwargs) + except Exception: + if strict(): + raise + count = 0 + newly_disabled = False + with _failure_lock: + count = _failures.get(site, 0) + 1 + _failures[site] = count + if count >= _MAX_FAILURES and site not in _disabled: + _disabled.add(site) + newly_disabled = True + if count == 1: + # Logged once per site, with the traceback. Repeats are silent: + # a hook that fails on every token of a streaming response would + # otherwise become the log volume. + logger.warning( + "failproofai_sdk: instrumentation hook %s failed; the instrumented call was " + "not affected. Set FAILPROOFAI_SDK_STRICT=1 to re-raise.", + site, + exc_info=True, + ) + else: + logger.debug("failproofai_sdk: instrumentation hook %s failed again (%d)", site, count) + if newly_disabled: + logger.error( + "failproofai_sdk: instrumentation hook %s failed %d times and is now disabled " + "for the rest of this process. Events from it will be missing.", + site, + count, + ) + return None + + +def safe(fn: Callable[..., Any]) -> Callable[..., Any]: + """Decorator form of `call_safely`. Put it on every callback an adapter exposes.""" + site = _site_of(fn) + + @functools.wraps(fn) + def _failproofai_safe(*args: Any, **kwargs: Any) -> Any: + return call_safely(fn, args, kwargs, site) + + _failproofai_safe.__failproofai_safe__ = True # type: ignore[attr-defined] + return _failproofai_safe + + +def _safe_call(fn: Callable[..., Any] | None, *args: Any, **kwargs: Any) -> Any: + if fn is None: + return None + return call_safely(fn, args, kwargs, _site_of(fn)) + + +# --------------------------------------------------------------------------- +# Shape A — wrapper surfaces +# --------------------------------------------------------------------------- + +def wrap_callable( + original: Callable[..., Any], + *, + before: Callable[..., Any] | None = None, + after: Callable[..., Any] | None = None, + on_error: Callable[..., Any] | None = None, +) -> Callable[..., Any]: + """Wrap a framework callable so start and end are one frame. + + * `before(*args, **kwargs)` -> an opaque ctx handed back to the others; + * `after(ctx, result)`; + * `on_error(ctx, exc)` — then the exception is re-raised, always. + + The structural guarantee, which is the whole reason this is a function and + not hand-written try/except in five adapters: **the user's call sits in + exactly one `try`, whose only job is to re-raise.** Nothing we do can change + what the wrapped callable returns or raises, because every one of our own + calls is outside that block and inside `call_safely`. That is auditable in + nine lines, and there is a test asserting the exception comes back out with + its `is` identity intact even when all three hooks raise. + """ + + @functools.wraps(original) + def _failproofai_wrapper(*args: Any, **kwargs: Any) -> Any: + ctx = _safe_call(before, *args, **kwargs) + try: + result = original(*args, **kwargs) + except BaseException as exc: # noqa: BLE001 - re-raised unconditionally + _safe_call(on_error, ctx, exc) + raise + _safe_call(after, ctx, result) + return result + + _failproofai_wrapper.__failproofai_wrapped__ = original # type: ignore[attr-defined] + return _failproofai_wrapper + + +def is_wrapped(obj: Any) -> bool: + return hasattr(obj, "__failproofai_wrapped__") + + +def unwrap(obj: Any) -> Any: + """The object we replaced, or `obj` itself if we never wrapped it.""" + return getattr(obj, "__failproofai_wrapped__", obj) + + +# --------------------------------------------------------------------------- +# Install / uninstall discipline +# --------------------------------------------------------------------------- + +class Patcher: + """Records what an `install()` replaced so `uninstall()` can put it back. + + Two rules, both of which exist because instrumentation libraries are + routinely installed alongside each other: + + 1. **Restore the saved object, never a re-import.** Re-importing to restore + hands back whatever the *current* value of the attribute's source is, + which is how two instrumentation libraries silently un-patch each other. + 2. **If the attribute is no longer ours, leave it alone.** Somebody patched + on top of us; restoring would delete their patch. We log at WARNING and + keep our record, so the customer can see it happened. + """ + + __slots__ = ("_records", "_lock") + + def __init__(self) -> None: + self._records: list[tuple[Any, str, Any, Any, bool]] = [] + self._lock = threading.Lock() + + def patch(self, obj: Any, attr: str, new: Any) -> None: + """Set `obj.attr = new`, remembering the exact object replaced.""" + existed = hasattr(obj, attr) + original = getattr(obj, attr, None) + try: + new.__failproofai_wrapped__ = original + except (AttributeError, TypeError): + # builtins, slots, C functions — the marker is best-effort, the + # identity check below falls back to `is` against what we stored. + pass + setattr(obj, attr, new) + with self._lock: + self._records.append((obj, attr, original, new, existed)) + + def restore_all(self) -> None: + """Undo every patch, newest first. Never raises.""" + with self._lock: + records = list(reversed(self._records)) + self._records.clear() + for obj, attr, original, installed, existed in records: + try: + current = getattr(obj, attr, None) + if current is not installed: + logger.warning( + "failproofai_sdk: not restoring %s.%s — it is no longer the object " + "failproofai_sdk installed (something else patched on top). Leaving " + "the current value in place rather than deleting their patch.", + getattr(obj, "__name__", type(obj).__name__), + attr, + ) + continue + if existed: + setattr(obj, attr, original) + else: + delattr(obj, attr) + except Exception: + logger.warning( + "failproofai_sdk: failed to restore %s.%s", obj, attr, exc_info=True + ) + + def __len__(self) -> int: + return len(self._records) + + +# --------------------------------------------------------------------------- +# Payload discipline +# --------------------------------------------------------------------------- + +TRUNCATION_MARKER = "…[truncated]" +FIELD_LIMIT = 8192 +EVENT_BUDGET = 32 * 1024 +_MAX_ITEMS = 100 +_MAX_DEPTH = 6 + + +class _Cut: + """Mutable 'did we cut anything' flag, threaded through the recursion.""" + + __slots__ = ("hit",) + + def __init__(self) -> None: + self.hit = False + + +def truncate(value: Any, limit: int = FIELD_LIMIT) -> Any: + """Shrink a payload value to something a column store will tolerate. + + Framework payloads are prompts, retrieved documents and tool outputs — the + three largest strings in the process. None of these are promoted columns, so + querying them means `JSONExtract` over the payload, which has already caused + a production memory blowup in the events store here. Payload discipline is not optional. + """ + return _truncate(value, limit, _Cut(), 0) + + +def _truncate(value: Any, limit: int, cut: _Cut, depth: int) -> Any: + if value is None or isinstance(value, (bool, int, float)): + return value + if isinstance(value, str): + if len(value) <= limit: + return value + cut.hit = True + return value[: max(limit - len(TRUNCATION_MARKER), 0)] + TRUNCATION_MARKER + if isinstance(value, bytes): + return _truncate(value.decode("utf-8", "replace"), limit, cut, depth) + if depth >= _MAX_DEPTH: + cut.hit = True + return _truncate(repr(value), limit, cut, _MAX_DEPTH) + # `Mapping`/`Sequence`, not `dict`/`list`. The concrete types missed every + # mapping a framework actually hands us that is not literally a dict — + # `MappingProxyType` (what `model_json_schema()` and any frozen config + # returns), `ChainMap`, and every third-party mapping — and those fell + # through to the repr branch at the bottom. A tool's JSON schema then + # reached the events store as the STRING + # `"mappingproxy({'title': 'From Unit', 'type': 'string'})"`: valid JSON + # holding a Python repr, so `JSONExtract` over it returns nothing and the + # field is unqueryable rather than merely ugly. Verified in a real stored + # row — a crewai `model_request.tools[0]…properties.from_unit`. + if isinstance(value, Mapping): + out = {} + for i, (k, v) in enumerate(value.items()): + if i >= _MAX_ITEMS: + cut.hit = True + out["…"] = f"[{len(value) - _MAX_ITEMS} more keys truncated]" + break + out[str(k)] = _truncate(v, limit, cut, depth + 1) + return out + # `str`/`bytes` are Sequences too and are handled above, so they cannot + # reach here; `Set` is a separate ABC and is not a `Sequence`. + if isinstance(value, (Sequence, AbcSet)): + items = list(value) + out_list = [_truncate(v, limit, cut, depth + 1) for v in items[:_MAX_ITEMS]] + if len(items) > _MAX_ITEMS: + cut.hit = True + out_list.append(f"[{len(items) - _MAX_ITEMS} more items truncated]") + return out_list + # A dataclass or a pydantic model is DATA, and every framework hands us + # them: a tool's argument model, its structured return, a settings object on + # a model request. They have no JSON shape by the checks above, so they were + # rendered — `Weather(city='Faro', celsius=21)` — which is a Python repr + # sitting inside a JSON string, unqueryable by `JSONExtract` and unfilterable + # in the dashboard. Each adapter was starting to unwrap them itself; doing it + # once here means an adapter that has not thought about it still records + # something readable. + shaped = _as_mapping(value) + if shaped is not None: + # Same depth, not depth + 1: the object is REPLACED by its mapping + # rather than nested inside one, and the Mapping branch above does the + # descending (and the per-field limits) from here. + return _truncate(shaped, limit, cut, depth) + + # An object with no JSON shape is rendered, not cut — `fw_truncated` means + # "data was lost", and a repr that fits has lost nothing a JSON encoder + # would have kept. + return _truncate(repr(value), limit, cut, _MAX_DEPTH) + + +def _as_mapping(value: Any) -> "dict | None": + """A dataclass instance or pydantic model as a plain dict, or None. + + Shallow on purpose. `dataclasses.asdict` and `model_dump` both recurse and + both COPY, so on a large object they duplicate the whole tree before + `_truncate` gets to decide it only wanted the first 8 KB. Reading the top + level and handing it back lets the existing walk apply the field limit, the + item cap and the depth cap on the way down, as it does for a dict. + + Everything here can execute the caller's own code — a pydantic validator, a + property behind `getattr` — so all of it is guarded, and a failure falls + through to `repr`, which is what happened before this existed. + """ + if isinstance(value, type): # the CLASS, not an instance of it + return None + if dataclasses.is_dataclass(value): + try: + return {f.name: getattr(value, f.name) for f in dataclasses.fields(value)} + except Exception: + return None + # `model_dump` and not `dict`: pydantic v2 names it distinctively, whereas + # half the objects in a typical process have some attribute called `dict` + # and calling it would be a coin flip. + dump = getattr(value, "model_dump", None) + if callable(dump): + try: + dumped = dump() + except Exception: + return None + return dumped if isinstance(dumped, Mapping) else None + return None + + +def _size(value: Any, _depth: int = 0) -> int: + if value is None or isinstance(value, (bool, int, float)): + return 8 + if isinstance(value, str): + return len(value) + if _depth >= _MAX_DEPTH: + return len(repr(value)) + # Same ABCs as `_truncate`, for the same reason: a size computed off `repr` + # for a value that `_truncate` will expand into JSON budgets the wrong + # number, and the budget is what decides which fields survive. + if isinstance(value, Mapping): + return sum(len(str(k)) + _size(v, _depth + 1) for k, v in value.items()) + if isinstance(value, (Sequence, AbcSet)): + return sum(_size(v, _depth + 1) for v in value) + return len(repr(value)) + + +def payload(fields: dict, *, limit: int = FIELD_LIMIT, budget: int = EVENT_BUDGET) -> dict: + """Apply the per-field limit and the per-event budget to a dict of extras. + + Anything cut sets `fw_truncated=True`, so a surprising-looking payload in + the dashboard is self-explaining rather than a mystery. + """ + out: dict[str, Any] = {} + remaining = budget + cut = _Cut() + for key, value in fields.items(): + shrunk = _truncate(value, limit, cut, 0) + if remaining <= 0: + cut.hit = True + continue + size = _size(shrunk) + if size > remaining: + cut.hit = True + shrunk = _truncate(shrunk, remaining, cut, 0) + size = _size(shrunk) + remaining -= size + out[key] = shrunk + if cut.hit: + out["fw_truncated"] = True + return out + + +# Every field name declared on any event dataclass, plus the five names +# `_events._RESERVED` blocks. Derived rather than hand-listed so that adding a +# field to `_schema.py` cannot leave a stale copy here. +def _declared_field_names() -> frozenset[str]: + names: set[str] = set(_RESERVED) + for obj in vars(_schema).values(): + if dataclasses.is_dataclass(obj) and isinstance(obj, type): + names.update(f.name for f in dataclasses.fields(obj)) + names.discard("extra_fields") + return frozenset(names) + + +# Deliberate exceptions: these are top-level by design. `duration_ms` is how an +# adapter reports a model call's real latency (it is not a declared parameter of +# `model_response`, and `durationOf` prefers it); `usage` is read by both the +# server summary and the dashboard as a token fallback; `request_id` pairs +# model events; `framework*` label every event. +ALLOWED_TOP_LEVEL = frozenset( + { + "request_id", + "duration_ms", + "usage", + "traceback", + "framework", + "framework_version", + "integration_version", + } +) + +# An extra whose name collides with a declared field SILENTLY OVERWRITES it: +# `_schema._build()` ends with `result.update(extra)`. An adapter reflecting a +# framework's kwargs into extras would then change `tool_name`, `model`, +# `outcome` or `input_tokens` — i.e. the promoted columns and the +# server's computed summary — and every test would still pass. +FORBIDDEN_EXTRAS = _declared_field_names() - ALLOWED_TOP_LEVEL + +_FW_PREFIX = "fw_" + + +def fw_fields(**kw: Any) -> dict: + """Build the `fw_*` extra-field namespace. + + fw_fields(run_id=run_id, node="retrieve", tags=None) + -> {"fw_run_id": "...", "fw_node": "retrieve"} + + Keys are prefixed unless they already are, or are one of the deliberate + top-level names. `None` values are dropped (the schema omits None optionals + anyway, and an extra explicitly set to None would still occupy a key). + Values go through `truncate`. Flat only — `payload_key_expr` on the server + is single-level, so a nested dict is not queryable. + """ + out: dict[str, Any] = {} + for key, value in kw.items(): + if value is None: + continue + if key in ALLOWED_TOP_LEVEL or key.startswith(_FW_PREFIX): + name = key + else: + name = _FW_PREFIX + key + out[name] = truncate(value) + return guard_extras(out) + + +def guard_extras(fields: dict) -> dict: + """Strip (or, in strict mode, reject) extras that would shadow a real field. + + Called on every emit, so even an adapter that builds its extras by hand + cannot silently rewrite a promoted column. + """ + bad = FORBIDDEN_EXTRAS & fields.keys() + if not bad: + return fields + names = sorted(bad) + message = ( + f"failproofai_sdk: extra fields {names} would overwrite declared event fields " + f"(schema merges extras last). Namespace them as fw_* instead." + ) + if strict(): + raise ValueError(message) + logger.warning("%s Dropping them.", message) + return {k: v for k, v in fields.items() if k not in bad} + + +def framework_fields(name: str, dist: str | None = None) -> dict: + """The `framework` / `framework_version` / `integration_version` triple. + + Payload-only, so **not** server-side filterable; promoting it later is a + five-file hand-mirrored change, so it is done on demand, not speculatively. + """ + out = {"framework": name, "integration_version": __version__} + version = _compat.version_string(dist) if dist else None + if version: + out["framework_version"] = version + return out + + +_ID_SEPARATORS = re.compile(r"[\s\-_.:/]+") +_EMBEDDED_UUID = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" + r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") +_HEX = frozenset("0123456789abcdefABCDEF") +_AGENT_ID_LIMIT = 64 + + +def normalize_agent_id(raw: Any, default: str = DEFAULT_AGENT_ID) -> str: + """Turn a framework's label into something safe for `agent_id`. + + `agent_id` is a `LowCardinality(String)` column and the primary facet on + every dashboard surface. A UUID in it poisons that facet permanently — + LowCardinality degrades, and the filter dropdown fills with one entry per + run. So a value that looks like an id becomes `default` and the real id goes + to `fw_agent_id` / `fw_run_id` where it belongs. + """ + if raw is None: + return default + text = " ".join(str(raw).split()) + if not text: + return default + if _looks_like_id(text): + return default + text = _strip_embedded_id(text) + if not text: + return default + return text[:_AGENT_ID_LIMIT] + + +def _looks_like_id(text: str) -> bool: + """True for UUIDs and long bare hex strings.""" + try: + uuid.UUID(text) + return True + except (ValueError, AttributeError, TypeError): + pass + bare = text.replace("-", "").replace("_", "") + return len(bare) >= 16 and all(c in _HEX for c in bare) + + +def _strip_embedded_id(text: str) -> str: + """Drop a per-run id that a readable prefix is carrying. + + `_looks_like_id` only fires on a value that is an id ALL THE WAY THROUGH, so + it caught a bare UUID and missed `agent-`, `crew_`, + `task-3f9a1c…` — a readable name with a per-run suffix, which is the shape + frameworks actually produce and precisely the one the docs warn against + ("a role containing a UUID, timestamp, or per-run suffix"). Those went + through untouched, one distinct value per run, into a + `LowCardinality(String)` column that is the primary facet on every dashboard + surface. That is the same poisoning the whole-string guard exists to stop, + reached by the more common route. + + Stripping rather than falling back to `default`: `agent-` still knows + it is an agent, and collapsing every such label to `main` would throw away + the one readable thing in it. A segment is dropped only if it is a UUID or a + hex run of 16+ characters, so a name like `agent-v2` or `step-3` is + untouched — and a value with nothing left after stripping falls back, which + is what the caller wanted for a bare id anyway. + """ + # Dashed UUIDs first, and as a substring: splitting on separators would + # break `task-3f9a1c2b-...` into five segments none of which is an id on its + # own, so the most standard shape of all would survive the segment pass. + stripped = _EMBEDDED_UUID.sub(" ", text) + parts = [p for p in _ID_SEPARATORS.split(stripped) if p] + kept = [p for p in parts if not _looks_like_id(p)] + # Nothing was an id: hand back the ORIGINAL, separators and all. Rejoining + # on spaces would rewrite every `node_a_b` in the process into `node a b`, + # which is a rename of the primary facet in exchange for nothing. (The + # empty segments a leading or trailing separator produces are dropped + # before the comparison, or `node_x_` alone would look like a change.) + if stripped == text and len(kept) == len(parts): + return text + return " ".join(kept).strip() + + +def ms(delta_seconds: Any) -> int: + """Whole milliseconds, as an `int`. + + The server stores `duration_ms` as a u32 and its JSON parser drops floats + (`as_u64()` -> None), so a float silently NULLs the column: the dashboard + then shows no duration and nobody sees an error. Negative deltas (clock + adjustments, a framework handing us an end before its start) clamp to 0. + """ + seconds = getattr(delta_seconds, "total_seconds", None) + value = seconds() if callable(seconds) else float(delta_seconds) + return max(round(value * 1000), 0) + + +# --------------------------------------------------------------------------- +# Shape B — callback surfaces +# --------------------------------------------------------------------------- + +@dataclasses.dataclass(slots=True) +class _Run: + identity: Identity + parent_key: Any + + +class RunTracker: + """Maps a framework's own run ids onto Failproof AI identity. + + This is Shape B: the surface where a start and its end are **separate + callbacks**, possibly on different threads (CrewAI dispatches handlers on a + ten-worker pool) or different asyncio tasks. Such an adapter can never use + contextvars — `ContextVar.reset(token)` raises `ValueError: Token was created + in a different Context` across tasks as well as threads, so a token cannot be + held between two callbacks. Instead we keep the mapping here and pass + `session_id=` / `agent_id=` **explicitly** on every emit. + + Bounded (`max_open`, FIFO eviction) because orphaned starts are normal: a + crashed run, a stream nobody consumed, a framework that forgot an end + callback. Unbounded, that is a memory leak in a long-lived server. + """ + + __slots__ = ("name", "_max_open", "_base_fields", "_runs", "_links", "_lock", "_warned") + + def __init__( + self, + name: str, + *, + max_open: int = 10_000, + base_fields: dict | None = None, + ) -> None: + self.name = name + self._max_open = max_open + self._base_fields = dict(base_fields or {}) + self._runs: dict[Any, _Run] = {} + self._links: dict[Any, Any] = {} + # RLock: `start_agent` resolves a parent while already holding it. + self._lock = threading.RLock() + self._warned = False + + # -- identity --------------------------------------------------------- + + def identity(self, key: Any, parent_key: Any = None, *, warn: bool = True) -> Identity | None: + """Resolve a run to an Failproof AI identity, in this order: + + 1. the exact `key`; + 2. the `parent_key` chain, walked through every link we have seen — a + framework's own parent_run_id is a *better* parent chain than a + contextvar stack, because it survives task hops and thread pools; + 3. **`failproofai_sdk.current()`** — this is the whole interop story. An + adapter running inside a hand-written `with failproofai_sdk.agent("planner")` + joins that same session and gets `parent_id="planner"`, so mixing the + manual API and an adapter produces one tree, not two; + 4. otherwise the event is dropped and we log **once**. + """ + with self._lock: + if key is not None: + run = self._runs.get(key) + if run is not None: + return run.identity + walked = self._walk(parent_key) + if walked is not None: + return walked + + ambient = self._ambient(coerce_agent=True) + if ambient is not None: + return ambient + + if warn: + self._warn_unresolved(key) + return None + + @staticmethod + def _ambient(*, coerce_agent: bool) -> Identity | None: + """Step 3: the identity a hand-written scope has bound, if any. + + The one copy of this. When resolving an event we coerce a missing + agent_id to `main`, but when resolving a *parent* we must not: inside a + bare `with failproofai_sdk.session(...)` there is no open agent, and claiming + `parent_id="main"` would point at an agent that never emitted an + `agent_start` — which makes the dashboard synthesize a never-ending root + span that stays `ongoing` forever. + """ + cur = _context.current() + if cur.session_id is None: + return None + return Identity( + session_id=cur.session_id, + agent_id=cur.agent_id or (DEFAULT_AGENT_ID if coerce_agent else None), + parent_id=cur.parent_id, + depth=cur.depth, + ) + + def _walk(self, parent_key: Any) -> Identity | None: + """Caller holds the lock.""" + seen: set[Any] = set() + key = parent_key + while key is not None and key not in seen: + seen.add(key) + run = self._runs.get(key) + if run is not None: + return run.identity + key = self._links.get(key) + return None + + def _warn_unresolved(self, key: Any) -> None: + if self._warned: + return + self._warned = True + logger.warning( + "failproofai_sdk: %s could not resolve a session for run %r and is dropping its " + "events. Wrap the call in `with failproofai_sdk.session():` (or " + "`with failproofai_sdk.agent(...):`) if you want them attributed. This is logged " + "once per tracker.", + self.name, + key, + ) + + def link(self, key: Any, parent_key: Any) -> None: + """Record a run's parent without making it an agent. + + Intermediate framework runs (a LangChain chain, a CrewAI task) do not + become Failproof AI spans, but their children still need to find the agent + above them. This is what makes step 2 of `identity()` work more than one + hop up. + """ + if key is None or parent_key is None or key == parent_key: + return + with self._lock: + self._evict(self._links) + self._links[key] = parent_key + + def _evict(self, table: dict) -> None: + """Caller holds the lock. FIFO — dicts keep insertion order.""" + while len(table) >= self._max_open: + table.pop(next(iter(table)), None) + + # -- agents ----------------------------------------------------------- + + def start_agent( + self, + key: Any, + *, + agent_id: str, + parent_key: Any = None, + session_id: str | None = None, + goal: str | None = None, + **fields: Any, + ) -> Identity: + """Register a run as an agent and emit `agent_start`.""" + parent = self._resolve_parent(parent_key) + sid = session_id or (parent.session_id if parent else None) or uuid.uuid4().hex + aid = normalize_agent_id(agent_id) + identity = Identity( + session_id=sid, + agent_id=aid, + parent_id=parent.agent_id if parent else None, + depth=(parent.depth + 1) if parent else 1, + ) + with self._lock: + self._evict(self._runs) + self._runs[key] = _Run(identity=identity, parent_key=parent_key) + if parent_key is not None: + self._evict(self._links) + self._links[key] = parent_key + self._emit( + "agent_start", + identity, + goal=truncate(goal) if goal is not None else None, + parent_id=identity.parent_id, + **fields, + ) + return identity + + def end_agent( + self, + key: Any, + *, + outcome: str = "success", + summary: str | None = None, + **fields: Any, + ) -> None: + """Emit `agent_end` and forget the run. + + `outcome` is `"failed"`, never `"failure"` — the server only counts + `error|failed|timeout|rejected` as a failure. + """ + with self._lock: + run = self._runs.pop(key, None) + identity = run.identity if run is not None else self.identity(key) + if identity is None: + return + self._emit( + "agent_end", + identity, + outcome=outcome, + summary=truncate(summary) if summary is not None else None, + **fields, + ) + + def open_agents(self) -> tuple[Any, ...]: + with self._lock: + return tuple(self._runs) + + def close_open_agents(self, *, outcome: str = "cancelled") -> None: + """Close every still-open agent, newest first. + + A session that dies with an open `agent_start` renders as `ongoing` + forever, so teardown closes what it opened. + """ + for key in reversed(self.open_agents()): + self.end_agent(key, outcome=outcome) + + def reset(self) -> None: + with self._lock: + self._runs.clear() + self._links.clear() + self._warned = False + + # -- everything else -------------------------------------------------- + + def emit(self, method: str, key: Any, *, parent_key: Any = None, **fields: Any) -> None: + """Emit any `failproofai_sdk.event.*` method against a run's identity. + + tracker.emit("tool_use", run_id, parent_key=parent_run_id, + tool_name=name, tool_call_id=str(run_id)) + + Drops the event (with one warning) when nothing resolves, rather than + inventing a session id: a synthesized session splits one run into many. + """ + if parent_key is not None: + self.link(key, parent_key) + identity = self.identity(key, parent_key) + if identity is None: + return + self._emit(method, identity, **fields) + + def _emit(self, method: str, identity: Identity, **fields: Any) -> None: + call_safely(self._emit_now, (method, identity, fields), {}, f"{self.name}.{method}") + + def _emit_now(self, method: str, identity: Identity, fields: dict) -> None: + # Two kinds of keyword here, and the split is by NAME, not by meaning: + # `fw_*` (plus whatever the adapter set as base fields) are payload + # extras and go through the guard and the size budget; everything else + # is a real parameter of the `event.*` method — `tool_name`, `input`, + # `outcome` — and is passed straight through. Those still get truncated, + # because `input`/`output`/`messages`/`content` are exactly the fields a + # framework fills with a 200KB prompt. + declared: dict[str, Any] = {} + extras: dict[str, Any] = {} + for key, value in fields.items(): + if value is None: + continue + if key.startswith(_FW_PREFIX): + extras[key] = value + else: + declared[key] = truncate(value) + merged = payload(guard_extras({**self._base_fields, **extras})) + # A base field named like a real parameter would be a duplicate keyword + # (TypeError inside the customer's callback); the explicit value wins. + merged = {k: v for k, v in merged.items() if k not in declared} + emit = getattr(_runtime.event, method) + emit(session_id=identity.session_id, agent_id=identity.agent_id, **declared, **merged) + + def _resolve_parent(self, parent_key: Any) -> Identity | None: + with self._lock: + walked = self._walk(parent_key) + if walked is not None: + return walked + # Same three steps as `identity()`, minus the exact-key lookup (a run + # cannot be its own parent) and minus the warning (a root agent with no + # ambient scope is normal, not a dropped event). + return self._ambient(coerce_agent=False) diff --git a/sdk/python/failproofai_sdk/integrations/crewai.py b/sdk/python/failproofai_sdk/integrations/crewai.py new file mode 100644 index 000000000..dadd0bced --- /dev/null +++ b/sdk/python/failproofai_sdk/integrations/crewai.py @@ -0,0 +1,1669 @@ +"""CrewAI adapter — written against crewai 1.15.8 (2026-07-29), floor 1.13.0. + + import failproofai_sdk, crewai + failproofai_sdk.instrument("crewai") + crew.kickoff() + +Everything here is a translation table over `crewai.events`, with exactly one +exception (6. below). Identity, correlation, payload budgets and the never-raise +policy live in `_core`. + +Six things about CrewAI that this file is shaped by, each verified against the +installed package rather than recalled: + +1. **The module is `crewai.events`, not `crewai.utilities.events`.** The old + shim was removed in 1.0.0. + +2. **Handlers are keyed by EXACT type, with no MRO walk** + (`event_bus._sync_handlers.get(type(event))`), so there is no `BaseEvent` + catch-all to register. Every event class is registered individually, which + is also what makes the anti-drift test possible. + +3. **Our handlers are `async def`, and that is a correctness requirement, not a + style choice.** `CrewAIEventsBus.emit()` dispatches *sync* handlers onto a + ten-worker `ThreadPoolExecutor`; with more than one worker, submission order + is not execution order. Measured on this version, 500 events through a sync + handler come back **out of order every single run**, while the same 500 + through an async handler are strictly ordered — async handlers are scheduled + with `run_coroutine_threadsafe` onto one background event loop, and + `call_soon_threadsafe` is FIFO. Out-of-order handling would break two of the + four rendering invariants at once: a `tool_result` could be written before + its `tool_use`, and an `agent_start` before the root's. Keep these `async`. + They must also never `await` anything, or that ordering guarantee is gone. + +4. **`BaseEvent` already carries the span tree** — `event_id`, + `parent_event_id`, and `started_event_id` on every `*Completed`/`*Failed` + event. Verified populated on tool, LLM, agent, task and crew events here, so + nesting is *read*, never reconstructed. (A per-`(role, tool)` LIFO is kept as + a fallback for the case where the pairing stack has been unwound by an + exception — that does happen, see 5.) + +5. **CrewAI's own agent executor emits `FlowStartedEvent`/`FlowFinishedEvent` + with `flow_name="AgentExecutor"`, nested inside every single + `agent_execution_started`.** Mapping flow events to agents unconditionally — + which is what the obvious reading of the API suggests — would put a spurious + `AgentExecutor` agent inside every agent execution, doubling the span tree and + poisoning the `agent_id` facet. A flow whose parent is an agent execution is + therefore a pass-through link, not an agent. Its `flow_finished` is also + *missing* when the agent's LLM call raises, so it can never be relied on to + close anything. + +6. **Not everything CrewAI does is on the bus.** `human_input=True` on a Task + goes through `crewai.core.providers.human_input`, which calls `input()` and + emits no event of any kind — verified on 1.15.16. That one surface is + instrumented by wrapping (`_patch_task_human_input`), which is the only patch + in this file; everything else here is a subscription. + +**Two kinds of node.** `_nodes` holds more than the spans that become agents: +tool calls and hook spans are recorded as *links* because CrewAI hangs real work +underneath them — a `delegate_work_to_coworker` call parents the coworker's whole +agent execution, and a flow method parents a `Crew.kickoff()` inside it. A leaf +that is not in `_nodes` is invisible to `_parent_key`, which then falls back to +the open root; the visible symptom is a flattened tree or a second session, never +an error. + +`agent_id` is the crew name or the agent **role** — `LowCardinality(String)`, +the primary dashboard facet. CrewAI's `agent.id` is a UUID and goes to +`fw_agent_id`, never here. +""" + +import json +import logging +import threading +import uuid +from collections import OrderedDict +from datetime import datetime, timezone +from typing import Any + +from failproofai_sdk.integrations import _compat +from failproofai_sdk.integrations._core import ( + RunTracker, + framework_fields, + fw_fields, + ms, + normalize_agent_id, + safe, + truncate, +) + +logger = logging.getLogger("failproofai_sdk.integrations") + +__all__ = ["adapter", "FailproofAICrewListener"] + +NAME = "crewai" +DIST = "crewai" + +# 1.13.0 is the release where `started_event_id` and a normalized `usage` dict +# are both present; below it the span tree has to be reconstructed by hand. +MIN_VERSION = "1.13.0" +# Ceiling: without one, a clean build a year from now pulls the next major, the +# event classes shift, and this adapter stops recording while raising nothing. +BELOW_VERSION = "2" + +_INSTALL_HINT = ( + "failproofai_sdk: cannot instrument 'crewai' because 'crewai.events' is not importable. " + "Install it with: pip install 'failproofai_sdk[crewai]' (or install crewai>=1.13 directly). " + "Note that the events package moved: it is 'crewai.events', not " + "'crewai.utilities.events', which was removed in crewai 1.0.0." +) + +try: # only ever executed by `instrument("crewai")` — `import failproofai_sdk` never gets here + from crewai.events import event_types as _ct + from crewai import events as _ce + from crewai.events.base_event_listener import BaseEventListener + from crewai.events.event_bus import crewai_event_bus +except ImportError as _exc: # pragma: no cover - exercised by uninstalling crewai + raise ImportError(_INSTALL_HINT) from _exc + + +# --------------------------------------------------------------------------- +# The translation table +# --------------------------------------------------------------------------- + +# event class name -> translator method name. One entry per class, because the +# bus does no MRO walk. Ordered as the dashboard reads them: structure, then +# leaves. +TABLE: tuple[tuple[str, str], ...] = ( + # --- structure: these become agent_start / agent_end ------------------- + ("CrewKickoffStartedEvent", "on_crew_started"), + ("CrewKickoffCompletedEvent", "on_crew_completed"), + ("CrewKickoffFailedEvent", "on_crew_failed"), + ("FlowStartedEvent", "on_flow_started"), + ("FlowFinishedEvent", "on_flow_finished"), + # A flow whose method raises emits `FlowFailedEvent` and NEVER + # `FlowFinishedEvent`. Without this row the flow's `agent_start` is never + # closed and the session renders `ongoing` forever — the worst outcome + # available here, and the one every other `*Failed` row in this table exists + # to avoid. + ("FlowFailedEvent", "on_flow_failed"), + ("AgentExecutionStartedEvent", "on_agent_started"), + ("AgentExecutionCompletedEvent", "on_agent_completed"), + ("AgentExecutionErrorEvent", "on_agent_error"), + # `Agent.kickoff()` — crewai's LiteAgent surface, an agent run with no Crew + # and no Task. It emits its OWN execution events, not the AgentExecution + # ones, and it is a ROOT: `parent_event_id` is None on it. Without these + # rows such a run produces no agent span at all — every LLM and tool event + # falls through to whatever ambient scope exists (agent_id "main"), or is + # dropped entirely when there is none. Nothing raises either way. + ("LiteAgentExecutionStartedEvent", "on_lite_agent_started"), + ("LiteAgentExecutionCompletedEvent", "on_agent_completed"), + ("LiteAgentExecutionErrorEvent", "on_agent_error"), + # --- structure we deliberately do NOT turn into spans ------------------ + # A CrewAI Task is a subset of the agent execution that runs it; emitting + # both would double every row and render them as siblings. The task is + # recorded as a link so its children still find the crew above them, and + # its id/name ride along on the agent's own events as fw_task_*. + ("TaskStartedEvent", "on_task_started"), + ("TaskCompletedEvent", "on_task_finished"), + ("TaskFailedEvent", "on_task_finished"), + # --- framework machinery around the agent: hook pairs ------------------ + ("MethodExecutionStartedEvent", "on_method_started"), + ("MethodExecutionFinishedEvent", "on_method_finished"), + ("MethodExecutionFailedEvent", "on_method_failed"), + ("LLMGuardrailStartedEvent", "on_guardrail_started"), + ("LLMGuardrailCompletedEvent", "on_guardrail_completed"), + # --- human in the loop ------------------------------------------------- + # A crew blocked on a person is otherwise an unexplained gap: no event + # fires for the whole wait, so the session reads as `ongoing` and its + # active duration absorbs however long the human took. The LangChain and + # LlamaIndex adapters both map their HITL surface onto the same four + # events, and this is the crewai one — `crewai.flow.runtime` emits the + # pair around every `@human_feedback` method. + ("HumanFeedbackRequestedEvent", "on_human_requested"), + ("HumanFeedbackReceivedEvent", "on_human_received"), + # --- things the agent calls: tool pairs -------------------------------- + ("ToolUsageStartedEvent", "on_tool_started"), + ("ToolUsageFinishedEvent", "on_tool_finished"), + ("ToolUsageErrorEvent", "on_tool_error"), + ("MemoryQueryStartedEvent", "on_store_started"), + ("MemoryQueryCompletedEvent", "on_store_finished"), + ("MemoryQueryFailedEvent", "on_store_failed"), + ("MemorySaveStartedEvent", "on_store_started"), + ("MemorySaveCompletedEvent", "on_store_finished"), + ("MemorySaveFailedEvent", "on_store_failed"), + ("MemoryRetrievalStartedEvent", "on_store_started"), + ("MemoryRetrievalCompletedEvent", "on_store_finished"), + ("MemoryRetrievalFailedEvent", "on_store_failed"), + ("KnowledgeQueryStartedEvent", "on_store_started"), + ("KnowledgeQueryCompletedEvent", "on_store_finished"), + ("KnowledgeQueryFailedEvent", "on_store_failed"), + ("KnowledgeRetrievalStartedEvent", "on_store_started"), + ("KnowledgeRetrievalCompletedEvent", "on_store_finished"), + ("KnowledgeSearchQueryFailedEvent", "on_store_failed"), + # --- model calls ------------------------------------------------------- + ("LLMCallStartedEvent", "on_llm_started"), + ("LLMCallCompletedEvent", "on_llm_completed"), + ("LLMCallFailedEvent", "on_llm_failed"), + # A streaming chunk emits NOTHING. A 500-token response would otherwise be + # 500 stored rows and 500 rail rows against a five-lane cap. It is + # folded into fw_chunks / fw_ttft_ms / fw_streamed on the model_response. + ("LLMStreamChunkEvent", "on_llm_chunk"), +) + +# Memory and knowledge operations are retrieval calls the agent makes, so they +# are tools, named for the surface they hit rather than the class that fired. +STORE_TOOLS: dict[str, str] = { + "MemoryQueryStartedEvent": "memory.query", + "MemoryQueryCompletedEvent": "memory.query", + "MemoryQueryFailedEvent": "memory.query", + "MemorySaveStartedEvent": "memory.save", + "MemorySaveCompletedEvent": "memory.save", + "MemorySaveFailedEvent": "memory.save", + "MemoryRetrievalStartedEvent": "memory.retrieve", + "MemoryRetrievalCompletedEvent": "memory.retrieve", + "MemoryRetrievalFailedEvent": "memory.retrieve", + "KnowledgeQueryStartedEvent": "knowledge.query", + "KnowledgeQueryCompletedEvent": "knowledge.query", + "KnowledgeQueryFailedEvent": "knowledge.query", + "KnowledgeRetrievalStartedEvent": "knowledge.search", + "KnowledgeRetrievalCompletedEvent": "knowledge.search", + "KnowledgeSearchQueryFailedEvent": "knowledge.search", +} + +# Flow names CrewAI uses for its own internal machinery. Belt and braces on top +# of the structural check in `on_flow_started` — the structural one is the rule, +# this is the fallback if a future release re-parents the executor flow. +INTERNAL_FLOW_NAMES = frozenset({"AgentExecutor"}) + +# Node kinds. "crew"/"flow"/"agent" are real Failproof AI agents; the rest are +# links that exist so a child can find the agent above it. +_AGENT_KINDS = frozenset({"crew", "flow", "agent"}) + +_MAX_NODES = 20_000 +_MAX_LEAVES = 10_000 +_MAX_STREAMS = 1_000 + + +class _Node: + """One CrewAI span, as far as we care about it.""" + + __slots__ = ("kind", "parent", "task", "agent_id") + + def __init__(self, kind, parent, task=None, agent_id=None): + self.kind = kind + self.parent = parent + self.task = task # (task_id, task_name), inherited from the parent + self.agent_id = agent_id + + +class _Leaf: + """An emitted-but-unclosed leaf span: a tool, a model call, a hook. + + `agent_end` force-closes open *pauses* but not tools, models or humans, so a + run that dies mid-tool leaves the session `ongoing` forever. Every open leaf + is remembered here and closed when the span that owns it ends. + """ + + __slots__ = ("method", "key", "parent_key", "match", "started", "fields") + + def __init__(self, method, key, parent_key, match, started, fields): + self.method = method + self.key = key + self.parent_key = parent_key + self.match = match # for the LIFO fallback when started_event_id is absent + self.started = started + self.fields = fields + + +class _CrewAIAdapter: + """The object `failproofai_sdk.integrations` loads as `adapter`.""" + + name = NAME + module = "crewai" + + def __init__(self) -> None: + # RLock: teardown resolves parents while already holding it. + self._lock = threading.RLock() + self._tracker: RunTracker | None = None + self._listener: "FailproofAICrewListener | None" = None + # (class, attribute, original descriptor, installed function) for the + # one wrapped surface — see `_patch_task_human_input`. + self._patched: list[tuple] = [] + self._nodes: "OrderedDict[str, _Node]" = OrderedDict() + self._leaves: "OrderedDict[str, _Leaf]" = OrderedDict() + self._streams: "OrderedDict[str, list]" = OrderedDict() + # pause id -> (emit key, parent key, prompt). Bounded like the rest: + # a run that is cancelled while a human is still thinking never sends + # the matching `received`, and unbounded that is a leak in a gateway. + self._pauses: "OrderedDict[str, tuple]" = OrderedDict() + self._roots: list[str] = [] + self._session_id: str | None = None + + # -- install / uninstall ------------------------------------------------ + + def install(self, **options: Any) -> None: + """Register the listener on the module-level `crewai_event_bus`. + + `options` is whatever was passed to `failproofai_sdk.instrument()`, and the same + dict reaches every adapter, so unknown keys are ignored rather than + raising a TypeError that would take out the other frameworks. + """ + _compat.check_version( + NAME, + DIST, + minimum=MIN_VERSION, + below=BELOW_VERSION, + reason="started_event_id on *Completed events, and a normalized usage dict", + ) + self._session_id = options.get("session_id") + self._tracker = RunTracker( + NAME, + base_fields=framework_fields(NAME, DIST), + ) + # Must be kept alive in a module-level global. `BaseEventListener` holds + # no reference back from the bus other than the bound handlers, so a + # listener that goes out of scope keeps "working" only by accident. + self._listener = FailproofAICrewListener(self) + self._patch_task_human_input() + + def uninstall(self) -> None: + listener, self._listener = self._listener, None + if listener is not None: + listener.teardown() + self._restore_patches() + # A run that is still open when the customer uninstruments would render + # `ongoing` forever. Close it, then forget everything. + self._close_everything(outcome="cancelled") + with self._lock: + self._nodes.clear() + self._leaves.clear() + self._streams.clear() + self._pauses.clear() + self._roots.clear() + if self._tracker is not None: + self._tracker.reset() + self._tracker = None + + # -- bookkeeping -------------------------------------------------------- + + def _note(self, event_id, parent, kind, task=None, agent_id=None) -> None: + with self._lock: + while len(self._nodes) >= _MAX_NODES: + self._nodes.pop(next(iter(self._nodes)), None) + inherited = task + if inherited is None and parent is not None: + node = self._nodes.get(parent) + inherited = node.task if node is not None else None + self._nodes[event_id] = _Node(kind, parent, inherited, agent_id) + + def _parent_key(self, parent_event_id, *, allow_root_fallback: bool = True): + """Which run a child should hang off. + + CrewAI's `parent_event_id` comes off a contextvar scope stack. On 1.15.16 + it survives crewai's own thread pool — measured: an `async_execution` + task's events all arrive with it set — so the fallback is for the events + whose parent span is genuinely gone: one that arrives after + `_close_span` popped its parent, or after `_MAX_NODES` evicted it. + Falling back to the open root keeps those inside the session instead of + minting a second one, which is the failure that splits one run into many. + + Root-capable events (`crew_kickoff_started`, `flow_started`) pass + `allow_root_fallback=False`: for them a missing parent genuinely means + "this is the top". + """ + with self._lock: + if parent_event_id is not None and parent_event_id in self._nodes: + return parent_event_id + if not allow_root_fallback or not self._roots: + return None + roots = list(self._roots) + if len(roots) == 1: + return roots[0] + return self._root_for_current_context(roots) + + def _root_for_current_context(self, roots): + """Which open root an orphaned event belongs to, with several open. + + `_roots` is process-global, so two crews kicked off on two threads leave + two entries in it and `roots[-1]` is a coin flip — one that files one + run's events under the OTHER run's session id. That is silent + cross-session corruption, which is strictly worse than a missing row, + and it is reachable without any `async_execution`: a late + `ToolUsageFinished` whose agent span has already been closed, or an + eviction at `_MAX_NODES`, is enough to reach this fallback. + + `CrewAIEventsBus.emit` copies the EMITTING thread's contextvars onto the + handler task, so an ambient `failproofai_sdk.session()` is a real signal about + which of the open runs we are inside. Prefer the newest root that agrees + with it; only guess when there is no ambient session to check against. + """ + tracker = self._tracker + if tracker is None: + return roots[-1] + ambient = tracker.identity(None, None, warn=False) + if ambient is None: + return roots[-1] + for key in reversed(roots): + identity = tracker.identity(key, warn=False) + if identity is not None and identity.session_id == ambient.session_id: + return key + return roots[-1] + + def _task_of(self, event): + """(task_id, task_name) for an event, from the event or its ancestors.""" + task_id = getattr(event, "task_id", None) + task_name = getattr(event, "task_name", None) + if task_id or task_name: + return (task_id, task_name) + with self._lock: + node = self._nodes.get(getattr(event, "parent_event_id", None)) + return node.task if node is not None else None + + # -- leaf spans --------------------------------------------------------- + + def _open_leaf(self, method, key, parent_key, match, started, fields) -> None: + with self._lock: + while len(self._leaves) >= _MAX_LEAVES: + self._leaves.pop(next(iter(self._leaves)), None) + self._leaves[key] = _Leaf(method, key, parent_key, match, started, fields) + + def _pop_leaf(self, start_id, match): + """The open leaf this ending event closes. + + `started_event_id` is the authority — it is populated on every + `*Completed`/`*Failed` event on this version. The `match` scan is the + documented fallback for the case where the scope stack was unwound by an + exception before the ending event was emitted: last-opened wins, which is + the right answer for a nested retry. + """ + with self._lock: + if start_id is not None: + leaf = self._leaves.pop(start_id, None) + if leaf is not None: + return leaf + for key in reversed(self._leaves): + if self._leaves[key].match == match: + return self._leaves.pop(key) + return None + + def _descends_from(self, node_id, ancestor) -> bool: + seen = set() + key = node_id + while key is not None and key not in seen: + if key == ancestor: + return True + seen.add(key) + node = self._nodes.get(key) + key = node.parent if node is not None else None + return False + + def _close_span(self, node_id, *, outcome, summary=None, **fields) -> None: + """End an agent span, after closing everything still open beneath it.""" + tracker = self._tracker + if tracker is None or node_id is None: + return + with self._lock: + leaves = [ + leaf + for leaf in reversed(list(self._leaves.values())) + if self._descends_from(leaf.parent_key, node_id) + ] + for leaf in leaves: + self._leaves.pop(leaf.key, None) + descendants = [ + key + for key in reversed(tracker.open_agents()) + if key != node_id and self._descends_from(key, node_id) + ] + for leaf in leaves: + self._force_close(leaf) + for key in descendants: + tracker.end_agent(key, outcome="cancelled", **fw_fields(closed_by="teardown")) + tracker.end_agent(node_id, outcome=outcome, summary=summary, **fields) + with self._lock: + self._roots = [key for key in self._roots if key != node_id] + for key in [k for k in self._nodes if self._descends_from(k, node_id)]: + self._nodes.pop(key, None) + + def _force_close(self, leaf) -> None: + """Close a leaf whose framework never reported an end.""" + tracker = self._tracker + if tracker is None: + return + fields = dict(leaf.fields) + fields.update(fw_fields(incomplete=True, closed_by="teardown")) + if leaf.method == "hook_completed": + fields["outcome"] = "cancelled" + if leaf.method == "model_response": + # Invariant: EVERY model_response carries an int duration_ms, including + # the ones we synthesize. Without it the dashboard shows no duration + # for exactly the calls that went wrong. + fields["duration_ms"] = ms(datetime.now(timezone.utc) - leaf.started) + tracker.emit(leaf.method, leaf.key, parent_key=leaf.parent_key, **fields) + + def _close_everything(self, *, outcome: str) -> None: + for node_id in list(reversed(self._roots)): + self._close_span(node_id, outcome=outcome) + tracker = self._tracker + if tracker is None: + return + with self._lock: + leaves = list(reversed(list(self._leaves.values()))) + self._leaves.clear() + for leaf in leaves: + self._force_close(leaf) + tracker.close_open_agents(outcome=outcome) + + # -- crew --------------------------------------------------------------- + + @safe + def on_crew_started(self, source, event) -> None: + tracker = self._tracker + if tracker is None: + return + # A fresh uuid4 per kickoff, NOT crew.id: crew.id is stable across + # kickoffs, so reusing it would merge every run of the same crew into one + # never-ending session. RunTracker mints it when no parent and no + # ambient scope supply one. + parent = self._parent_key(event.parent_event_id, allow_root_fallback=False) + agent_id = normalize_agent_id(getattr(event, "crew_name", None), default="crew") + tracker.start_agent( + event.event_id, + agent_id=agent_id, + parent_key=parent, + session_id=self._session_id if parent is None else None, + goal=_first_text(getattr(event, "inputs", None)), + **fw_fields( + kind="crew", + crew_name=getattr(event, "crew_name", None), + inputs=getattr(event, "inputs", None), + event_id=event.event_id, + ), + ) + self._note(event.event_id, parent, "crew", agent_id=agent_id) + with self._lock: + if parent is None: + self._roots.append(event.event_id) + + @safe + def on_crew_completed(self, source, event) -> None: + self._close_span( + self._crew_key(event), + outcome="success", + summary=_text(getattr(event, "output", None)), + **fw_fields(total_tokens=getattr(event, "total_tokens", None)), + ) + + @safe + def on_crew_failed(self, source, event) -> None: + # The error is reported on the span that OWNS it. No standalone `error` + # event: `sessionSummary.errorCount` counts both, so emitting one here + # would double-count every failed crew. + self._close_span( + self._crew_key(event), + outcome="failed", + summary=_text(getattr(event, "error", None)), + **fw_fields(error=_text(getattr(event, "error", None))), + ) + + def _crew_key(self, event): + """The crew span this ending event closes. + + `started_event_id` is set explicitly by `Crew._finish_execution`, so it + is reliable; the root fallback exists because a session left with an open + root `agent_start` renders `ongoing` forever, which is the single worst + outcome available here. + """ + with self._lock: + node_id = getattr(event, "started_event_id", None) + if node_id is not None and node_id in self._nodes: + return node_id + roots = list(self._roots) + if not roots: + return None + # Same reasoning as `_root_for_current_context`: closing "the newest + # root" while a second crew is open on another thread ends the WRONG + # crew, which reads as one run finishing early and one hanging forever. + return roots[0] if len(roots) == 1 else self._root_for_current_context(roots) + + # -- flows -------------------------------------------------------------- + + @safe + def on_flow_started(self, source, event) -> None: + tracker = self._tracker + if tracker is None: + return + parent_id = getattr(event, "parent_event_id", None) + with self._lock: + parent_node = self._nodes.get(parent_id) if parent_id else None + internal = (parent_node is not None and parent_node.kind == "agent") or ( + getattr(event, "flow_name", None) in INTERNAL_FLOW_NAMES + ) + if internal: + # CrewAI's own agent executor. Link it so the LLM and tool events + # underneath resolve to the agent, but emit nothing: it is not a + # flow the user wrote, and its flow_finished is missing entirely + # when the agent's LLM call raises. + parent = self._parent_key(parent_id) + tracker.link(event.event_id, parent) + self._note(event.event_id, parent, "flow_internal") + return + parent = self._parent_key(parent_id, allow_root_fallback=False) + agent_id = normalize_agent_id(getattr(event, "flow_name", None), default="flow") + tracker.start_agent( + event.event_id, + agent_id=agent_id, + parent_key=parent, + session_id=self._session_id if parent is None else None, + goal=_first_text(getattr(event, "inputs", None)), + **fw_fields( + kind="flow", + flow_name=getattr(event, "flow_name", None), + inputs=getattr(event, "inputs", None), + event_id=event.event_id, + ), + ) + self._note(event.event_id, parent, "flow", agent_id=agent_id) + with self._lock: + if parent is None: + self._roots.append(event.event_id) + + @safe + def on_flow_finished(self, source, event) -> None: + node_id = self._flow_key(event) + if node_id is None: + return + self._close_span( + node_id, outcome="success", summary=_text(getattr(event, "result", None)) + ) + + @safe + def on_flow_failed(self, source, event) -> None: + node_id = self._flow_key(event) + if node_id is None: + return + error = _text(getattr(event, "error", None)) + self._close_span( + node_id, + outcome="failed", + summary=error, + **fw_fields(error=error), + ) + + def _flow_key(self, event): + """The flow span this ending event closes, or None if there is nothing to close. + + `flow_internal` is the AgentExecutor pass-through link (see 5. in the + module docstring): it opened no span, so its end just drops the node. + """ + node_id = getattr(event, "started_event_id", None) + with self._lock: + node = self._nodes.get(node_id) if node_id else None + if node is not None and node.kind == "flow_internal": + self._nodes.pop(node_id, None) + return None + if node is None: + return None + return node_id + + # -- tasks: recorded, never emitted ------------------------------------- + + @safe + def on_task_started(self, source, event) -> None: + tracker = self._tracker + if tracker is None: + return + parent = self._parent_key(event.parent_event_id) + tracker.link(event.event_id, parent) + task = (getattr(event, "task_id", None), getattr(event, "task_name", None)) + self._note(event.event_id, parent, "task", task=task) + + @safe + def on_task_finished(self, source, event) -> None: + with self._lock: + self._nodes.pop(event.started_event_id, None) + + # -- agents ------------------------------------------------------------- + + @safe + def on_agent_started(self, source, event) -> None: + tracker = self._tracker + if tracker is None: + return + agent = getattr(event, "agent", None) + parent = self._parent_key(event.parent_event_id) + # `event.agent_role` is None on this event (only the tool and LLM events + # get it filled in), so the role comes off the agent object. The UUID in + # `agent.id` must never reach agent_id. + role = getattr(event, "agent_role", None) or getattr(agent, "role", None) + agent_id = normalize_agent_id(role, default="agent") + task = self._task_of(event) or (None, None) + tracker.start_agent( + event.event_id, + agent_id=agent_id, + parent_key=parent, + goal=_text(getattr(agent, "goal", None)), + **fw_fields( + kind="agent", + agent_id=str(getattr(agent, "id", "")) or None, + agent_role=role, + task_id=task[0], + task_name=task[1], + tools=_tool_names(getattr(event, "tools", None)), + allow_delegation=getattr(agent, "allow_delegation", None), + event_id=event.event_id, + ), + ) + self._note(event.event_id, parent, "agent", task=task, agent_id=agent_id) + + @safe + def on_lite_agent_started(self, source, event) -> None: + """`Agent.kickoff()`. A root, like a crew or a user-written flow. + + The role is only in `agent_info` here — `agent_role` is None on this + event, exactly as it is on `AgentExecutionStartedEvent`, and + `agent_info["id"]` is a UUID that must not reach `agent_id`. + """ + tracker = self._tracker + if tracker is None: + return + info = getattr(event, "agent_info", None) + info = info if isinstance(info, dict) else {} + parent = self._parent_key( + getattr(event, "parent_event_id", None), allow_root_fallback=False + ) + role = getattr(event, "agent_role", None) or info.get("role") + agent_id = normalize_agent_id(role, default="agent") + tracker.start_agent( + event.event_id, + agent_id=agent_id, + parent_key=parent, + session_id=self._session_id if parent is None else None, + goal=_text(info.get("goal")), + **fw_fields( + kind="agent", + lite=True, + agent_id=str(info.get("id") or "") or None, + agent_role=role, + tools=_tool_names(getattr(event, "tools", None)), + event_id=event.event_id, + ), + ) + self._note(event.event_id, parent, "agent", agent_id=agent_id) + with self._lock: + if parent is None: + self._roots.append(event.event_id) + + @safe + def on_agent_completed(self, source, event) -> None: + self._close_span( + event.started_event_id, + outcome="success", + summary=_text(getattr(event, "output", None)), + ) + + @safe + def on_agent_error(self, source, event) -> None: + self._close_span( + event.started_event_id, + outcome="failed", + summary=_text(getattr(event, "error", None)), + **fw_fields(error=_text(getattr(event, "error", None))), + ) + + # -- flow methods and guardrails: hook pairs ---------------------------- + + @safe + def on_method_started(self, source, event) -> None: + self._hook_start( + event, + hook_name=getattr(event, "method_name", None) or "flow_method", + trigger_event="flow_method", + input=_dictify(getattr(event, "params", None)), + extra=fw_fields(flow_name=getattr(event, "flow_name", None)), + ) + + @safe + def on_method_finished(self, source, event) -> None: + self._hook_end(event, outcome="success", output=_text(getattr(event, "result", None))) + + @safe + def on_method_failed(self, source, event) -> None: + self._hook_end( + event, outcome="failed", error=_text(getattr(event, "error", None)) + ) + + # -- human in the loop -------------------------------------------------- + # + # The same four events, in the same order, as the LangChain and LlamaIndex + # adapters. Neither pair is redundant and neither is optional: + # + # human_wait -> human_input carries the prompt, the answer and the + # pendingHuman count; + # agent_pause -> agent_resume is the ONLY thing that feeds pausedMs, so + # without it the wait is billed as active + # time on the agent. + # + # PAIRING is the whole difficulty here. Verified against crewai 1.15.16: + # `crewai.flow.runtime` constructs both events with **no correlation id at + # all** — `request_id` is None on both and `started_event_id` is None on the + # received one, so there is nothing to join on. What they do share is + # `(flow_name, method_name)`, and the interaction is strictly sequential: + # the runtime emits `requested`, blocks on `input()`, then emits `received`. + # + # So the lookup is `request_id` -> `(flow_name, method_name)` -> most + # recently opened, in that order. The last fallback is sound rather than a + # guess, because a blocking console prompt cannot interleave with another. + # `request_id` is tried first anyway: the enterprise async provider in + # `crewai.flow.async_feedback` does set it, and that one CAN interleave. + + @staticmethod + def _pause_lookup_keys(event) -> tuple: + """Join keys for one HITL event, most specific first.""" + keys = [] + request_id = getattr(event, "request_id", None) + if request_id: + keys.append(("request", str(request_id))) + started = getattr(event, "started_event_id", None) + if started: + keys.append(("request", str(started))) + flow = getattr(event, "flow_name", None) + method = getattr(event, "method_name", None) + if flow or method: + keys.append(("method", str(flow), str(method))) + return tuple(keys) + + @safe + def on_human_requested(self, source, event) -> None: + tracker = self._tracker + if tracker is None: + return + # The id the SDK pairs on. `event_id` is always present and unique; + # `request_id` is usually None, so it cannot serve as this. + pause_id = str(getattr(event, "event_id", None) or uuid.uuid4().hex) + parent = self._parent_key(getattr(event, "parent_event_id", None)) + prompt = _text(getattr(event, "message", None)) + record = (pause_id, parent, prompt) + + with self._lock: + while len(self._pauses) >= _MAX_NODES: + self._pauses.pop(next(iter(self._pauses)), None) + for key in self._pause_lookup_keys(event): + self._pauses[key] = record + # Always keyed by its own id too, so a `received` that does carry an + # id finds it even when flow and method are both blank. + self._pauses[("request", pause_id)] = record + + options = [str(o) for o in (getattr(event, "emit", None) or [])] or None + extra = fw_fields( + flow_name=getattr(event, "flow_name", None), + method_name=getattr(event, "method_name", None), + output=_text(getattr(event, "output", None)), + ) + tracker.emit( + "human_wait", + pause_id, + parent_key=parent, + input_id=pause_id, + prompt=prompt, + options=options, + reason="crewai_human_feedback", + **extra, + ) + tracker.emit( + "agent_pause", + pause_id, + parent_key=parent, + pause_id=pause_id, + reason="crewai_human_feedback", + **extra, + ) + + @safe + def on_human_received(self, source, event) -> None: + tracker = self._tracker + if tracker is None: + return + feedback = _text(getattr(event, "feedback", None)) + extra = fw_fields( + flow_name=getattr(event, "flow_name", None), + method_name=getattr(event, "method_name", None), + outcome_hint=getattr(event, "outcome", None), + ) + + record = None + with self._lock: + for key in self._pause_lookup_keys(event): + record = self._pauses.pop(key, None) + if record is not None: + break + if record is None and self._pauses: + # Sequential fallback: the most recently opened pause. + _, record = self._pauses.popitem(last=True) + if record is not None: + # Drop this record's other alias keys so it cannot pair twice. + for key in [k for k, v in self._pauses.items() if v is record]: + self._pauses.pop(key, None) + + if record is None: + # Feedback for a request we never saw — a flow resumed in another + # process, or one that predates instrument(). Record the answer but + # NOT agent_resume: closing a pause that never opened subtracts a + # pausedMs interval that was never added. `input_id` falls back to + # this event's own id and is NEVER None, which is a hard TypeError + # on `human_input` rather than a quietly dropped field. + own_id = str(getattr(event, "event_id", None) or uuid.uuid4().hex) + tracker.emit( + "human_input", + own_id, + parent_key=self._parent_key(getattr(event, "parent_event_id", None)), + input_id=own_id, + response=feedback, + **fw_fields(orphaned=True), + **extra, + ) + return + + pause_id, parent, _prompt = record + # agent_resume FIRST: the dashboard closes the pause on it, and + # `duration_ms` on both closing events is measured from the matching + # opening one. + tracker.emit( + "agent_resume", + pause_id, + parent_key=parent, + pause_id=pause_id, + reason="crewai_human_feedback", + **extra, + ) + tracker.emit( + "human_input", + pause_id, + parent_key=parent, + input_id=pause_id, + response=feedback, + **extra, + ) + + # -- the OTHER human-in-the-loop surface: Task(human_input=True) --------- + # + # crewai has TWO of them and only one is on the event bus. + # `@human_feedback` on a Flow method emits the pair mapped above. + # `human_input=True` on a crew Task — the surface crewai's own Crew + # documentation teaches, and the one most people mean by "HITL in crewai" — + # runs through `crewai.core.providers.human_input.SyncHumanInputProvider`, + # which prints a rich panel, calls `input()`, and emits NOTHING. Verified + # against crewai 1.15.16: no event class in `crewai.events` fires for it. + # + # Subscribing therefore cannot reach it, so this one surface is instrumented + # by WRAPPING. It is the only patch in this adapter. Three properties keep + # that acceptable: + # + # * `_prompt_input` is the narrowest possible seam — it is exactly the + # blocking call, so the pause interval is the human's wait and nothing + # else. Wrapping `handle_feedback` instead would fold the agent's + # re-invocation LLM calls into pausedMs. + # * the call is inside one `try` whose only job is to re-raise, and both + # of our own hooks are `@safe`, so nothing here can change what the + # customer's crew returns or raises. + # * if crewai moves it, `_compat.probe` disables this pair and the rest of + # the adapter is untouched. + + _HITL_PROMPT = ( + "crewai is blocked on human feedback for this task's result " + "(an empty answer accepts it)." + ) + _HITL_REASON = "crewai_task_human_input" + + def _patch_task_human_input(self) -> None: + try: + from crewai.core.providers.human_input import SyncHumanInputProvider + except Exception: # pragma: no cover - exercised by a crewai that moved it + _compat.probe(NAME, "Task(human_input=True)", lambda: False) + return + for attr, is_async in (("_prompt_input", False), ("_prompt_input_async", True)): + original = getattr(SyncHumanInputProvider, attr, None) + if original is None: + _compat.probe(NAME, f"SyncHumanInputProvider.{attr}", lambda: False) + continue + if getattr(original, "__failproofai_wrapped__", None) is not None: + # Already ours — an install that ran without its uninstall. + # Wrapping again would emit the pause twice per prompt. + continue + wrapper = self._wrap_prompt(original, is_async=is_async) + wrapper.__failproofai_wrapped__ = original + # `staticmethod(...)`, not the bare function: `_prompt_input` is a + # staticmethod and the call site is `self._prompt_input(crew)`, so a + # plain function would bind and arrive with `self` as `crew`. + descriptor = SyncHumanInputProvider.__dict__.get(attr) + setattr(SyncHumanInputProvider, attr, staticmethod(wrapper)) + self._patched.append((SyncHumanInputProvider, attr, descriptor, wrapper)) + + def _restore_patches(self) -> None: + for klass, attr, descriptor, installed in reversed(self._patched): + try: + if getattr(klass, attr, None) is not installed: + # Somebody patched on top of us; restoring would delete + # their patch. Same rule as `_core.Patcher`. + logger.warning( + "failproofai_sdk: not restoring %s.%s — it is no longer the object " + "failproofai_sdk installed.", + klass.__name__, + attr, + ) + continue + if descriptor is not None: + setattr(klass, attr, descriptor) + else: + delattr(klass, attr) + except Exception: # pragma: no cover - teardown must never raise + logger.warning( + "failproofai_sdk: failed to restore %s.%s", klass, attr, exc_info=True + ) + self._patched.clear() + + def _wrap_prompt(self, original, *, is_async): + adapter = self + + if is_async: + async def _failproofai_prompt(*args, **kwargs): + record = adapter._human_wait_start(args[0] if args else None) + try: + answer = await original(*args, **kwargs) + except BaseException as exc: # noqa: BLE001 - re-raised below + adapter._human_wait_end(record, None, exc) + raise + adapter._human_wait_end(record, answer, None) + return answer + else: + def _failproofai_prompt(*args, **kwargs): + record = adapter._human_wait_start(args[0] if args else None) + try: + answer = original(*args, **kwargs) + except BaseException as exc: # noqa: BLE001 - re-raised below + adapter._human_wait_end(record, None, exc) + raise + adapter._human_wait_end(record, answer, None) + return answer + + _failproofai_prompt.__name__ = getattr(original, "__name__", "_prompt_input") + _failproofai_prompt.__qualname__ = "failproofai_sdk.crewai.task_human_input" + return _failproofai_prompt + + @safe + def _human_wait_start(self, crew): + tracker = self._tracker + if tracker is None: + return None + pause_id = uuid.uuid4().hex + parent = self._current_agent_key() + extra = fw_fields( + surface="task_human_input", + crew_name=getattr(crew, "name", None), + ) + tracker.emit( + "human_wait", + pause_id, + parent_key=parent, + input_id=pause_id, + prompt=self._HITL_PROMPT, + reason=self._HITL_REASON, + **extra, + ) + tracker.emit( + "agent_pause", + pause_id, + parent_key=parent, + pause_id=pause_id, + reason=self._HITL_REASON, + **extra, + ) + return (pause_id, parent) + + @safe + def _human_wait_end(self, record, answer, error): + tracker = self._tracker + if tracker is None or record is None: + return + pause_id, parent = record + extra = fw_fields(surface="task_human_input", error=_text(str(error)) if error else None) + # agent_resume FIRST, for the same reason as the flow pair above. + tracker.emit( + "agent_resume", + pause_id, + parent_key=parent, + pause_id=pause_id, + reason=self._HITL_REASON, + **extra, + ) + tracker.emit( + "human_input", + pause_id, + parent_key=parent, + input_id=pause_id, + response=_text(answer), + **extra, + ) + + def _current_agent_key(self): + """The innermost open agent span — what a blocking prompt sits inside. + + There is no event to read a parent off here, so it comes from the node + table. Newest agent wins (a console prompt cannot interleave with + another), except that with two crews open on two threads the newest is a + coin flip, so an ambient session narrows it the same way + `_root_for_current_context` does. + """ + tracker = self._tracker + with self._lock: + candidates = [key for key, node in self._nodes.items() if node.kind == "agent"] + if not candidates: + return self._parent_key(None) + if len(candidates) == 1 or tracker is None: + return candidates[-1] + ambient = tracker.identity(None, None, warn=False) + if ambient is None: + return candidates[-1] + for key in reversed(candidates): + identity = tracker.identity(key, warn=False) + if identity is not None and identity.session_id == ambient.session_id: + return key + return candidates[-1] + + @safe + def on_guardrail_started(self, source, event) -> None: + self._hook_start( + event, + hook_name=getattr(event, "guardrail_name", None) or "guardrail", + trigger_event="guardrail", + input=None, + extra=fw_fields( + guardrail_type=getattr(event, "guardrail_type", None), + retry_count=getattr(event, "retry_count", None), + ), + ) + + @safe + def on_guardrail_completed(self, source, event) -> None: + # "rejected" is in the server's failure vocabulary + # (error|failed|timeout|rejected), so a tripped guardrail paints red + # rather than reading as a successful hook that happened to say no. + passed = bool(getattr(event, "success", False)) + self._hook_end( + event, + outcome="success" if passed else "rejected", + output=_text(getattr(event, "result", None)), + error=_text(getattr(event, "error", None)), + extra=fw_fields(retry_count=getattr(event, "retry_count", None)), + ) + + def _hook_start(self, event, *, hook_name, trigger_event, input, extra) -> None: + tracker = self._tracker + if tracker is None: + return + parent = self._parent_key(event.parent_event_id) + fields = dict( + hook_name=hook_name, + hook_id=event.event_id, + trigger_event=trigger_event, + input=input, + **extra, + ) + tracker.emit("hook_triggered", event.event_id, parent_key=parent, **fields) + # A flow method is a PARENT: a `Crew.kickoff()` inside one arrives with + # `parent_event_id` set to this event. Unless the id is a node, + # `_parent_key` cannot see it, `on_crew_started` reads "no parent" and + # mints a SECOND ROOT — a whole separate session for a crew that ran + # inside the flow. See `_tool_start` for the same rule. + self._note(event.event_id, parent, "hook") + self._open_leaf( + "hook_completed", + event.event_id, + parent, + ("hook", hook_name), + event.timestamp, + {"hook_name": hook_name, "hook_id": event.event_id}, + ) + + def _hook_end(self, event, *, outcome, output=None, error=None, extra=None) -> None: + tracker = self._tracker + if tracker is None: + return + leaf = self._pop_leaf(event.started_event_id, ("hook", _hook_name_of(event))) + if leaf is None: + return + with self._lock: + self._nodes.pop(leaf.key, None) + # duration_ms is auto-computed by the SDK from the hook_triggered we + # emitted, and is hard-rejected from callers on hook_completed. + tracker.emit( + "hook_completed", + leaf.key, + parent_key=leaf.parent_key, + hook_name=leaf.fields["hook_name"], + hook_id=leaf.fields["hook_id"], + outcome=outcome, + output=output, + error=error, + **(extra or {}), + ) + + # -- tools -------------------------------------------------------------- + + @safe + def on_tool_started(self, source, event) -> None: + self._tool_start( + event, + tool_name=getattr(event, "tool_name", None) or "tool", + input=_dictify(getattr(event, "tool_args", None)), + extra=fw_fields( + tool_class=getattr(event, "tool_class", None), + run_attempts=getattr(event, "run_attempts", None), + agent_role=getattr(event, "agent_role", None), + agent_id=getattr(event, "agent_id", None), + ), + ) + + @safe + def on_tool_finished(self, source, event) -> None: + self._tool_end( + event, + output=_text(getattr(event, "output", None)), + error=None, + extra=fw_fields( + from_cache=getattr(event, "from_cache", None), + run_attempts=getattr(event, "run_attempts", None), + ), + ) + + @safe + def on_tool_error(self, source, event) -> None: + # A tool failure the agent loop catches and retries is not a run-level + # error, so it is reported on the tool_result and nowhere else. + self._tool_end( + event, + output=None, + error=_text(getattr(event, "error", None)) or "tool failed", + extra=fw_fields(run_attempts=getattr(event, "run_attempts", None)), + ) + + @safe + def on_store_started(self, source, event) -> None: + name = STORE_TOOLS.get(type(event).__name__, "memory") + self._tool_start( + event, + tool_name=name, + input=_dictify(getattr(event, "query", None) or getattr(event, "value", None)), + extra=fw_fields(limit=getattr(event, "limit", None), store=name), + ) + + @safe + def on_store_finished(self, source, event) -> None: + results = ( + getattr(event, "results", None) + or getattr(event, "memory_content", None) + or getattr(event, "retrieved_knowledge", None) + ) + self._tool_end( + event, + output=truncate(results), + error=None, + extra=fw_fields( + query_time_ms=getattr(event, "query_time_ms", None), + save_time_ms=getattr(event, "save_time_ms", None), + retrieval_time_ms=getattr(event, "retrieval_time_ms", None), + ), + ) + + @safe + def on_store_failed(self, source, event) -> None: + self._tool_end( + event, + output=None, + error=_text(getattr(event, "error", None)) or "store operation failed", + extra={}, + ) + + def _tool_start(self, event, *, tool_name, input, extra) -> None: + tracker = self._tracker + if tracker is None: + return + parent = self._parent_key(event.parent_event_id) + # tool_call_id is CrewAI's own event_id, verbatim: it is a uuid4, so it + # cannot collide inside `_pending`, and it lines our rows up with the + # framework's own logs. + tracker.emit( + "tool_use", + event.event_id, + parent_key=parent, + tool_name=tool_name, + tool_call_id=event.event_id, + input=input, + **extra, + ) + # A tool call is a PARENT, not only a leaf. `delegate_work_to_coworker` + # and `ask_question_to_coworker` run a whole coworker underneath the + # call: verified on crewai 1.15.16, the delegate's + # `AgentExecutionStartedEvent.parent_event_id` is exactly this event's id. + # Unless that id is a node, `_parent_key` misses it, falls back to the + # open root, and every delegated agent is re-parented onto the CREW — + # manager and coworker rendered as siblings, which is the whole + # hierarchical process flattened into one level. + self._note(event.event_id, parent, "tool") + self._open_leaf( + "tool_result", + event.event_id, + parent, + ("tool", getattr(event, "agent_role", None), tool_name), + event.timestamp, + {"tool_name": tool_name, "tool_call_id": event.event_id}, + ) + + def _tool_end(self, event, *, output, error, extra) -> None: + tracker = self._tracker + if tracker is None: + return + name = getattr(event, "tool_name", None) or STORE_TOOLS.get( + type(event).__name__, "tool" + ) + leaf = self._pop_leaf( + event.started_event_id, ("tool", getattr(event, "agent_role", None), name) + ) + if leaf is None: + return + with self._lock: + self._nodes.pop(leaf.key, None) + # duration_ms is hard-rejected on tool_result and is computed by the SDK + # from the tool_use we emitted a moment ago. + tracker.emit( + "tool_result", + leaf.key, + parent_key=leaf.parent_key, + tool_name=leaf.fields["tool_name"], + tool_call_id=leaf.fields["tool_call_id"], + output=output, + error=error, + **extra, + ) + + # -- model calls -------------------------------------------------------- + + @safe + def on_llm_started(self, source, event) -> None: + tracker = self._tracker + if tracker is None: + return + parent = self._parent_key(event.parent_event_id) + call_id = getattr(event, "call_id", None) or event.event_id + task = self._task_of(event) or (None, None) + tracker.emit( + "model_request", + event.event_id, + parent_key=parent, + model=getattr(event, "model", None), + messages=_messages(getattr(event, "messages", None)), + tools=_listify(getattr(event, "tools", None)), + request_id=call_id, + **fw_fields( + call_id=call_id, + temperature=getattr(event, "temperature", None), + max_tokens=getattr(event, "max_tokens", None), + stream=getattr(event, "stream", None), + task_id=task[0], + task_name=task[1], + ), + ) + self._open_leaf( + "model_response", + event.event_id, + parent, + ("llm", call_id), + event.timestamp, + {"model": getattr(event, "model", None), "request_id": call_id}, + ) + + @safe + def on_llm_completed(self, source, event) -> None: + usage = getattr(event, "usage", None) + input_tokens, output_tokens, normalized = _tokens(usage) + self._model_end( + event, + content=_text(getattr(event, "response", None)), + stop_reason=getattr(event, "finish_reason", None), + error=None, + extra=dict( + input_tokens=input_tokens, + output_tokens=output_tokens, + usage=normalized or None, + **fw_fields( + usage_raw=usage if isinstance(usage, dict) else None, + response_id=getattr(event, "response_id", None), + call_type=_enum_value(getattr(event, "call_type", None)), + ), + ), + ) + + @safe + def on_llm_failed(self, source, event) -> None: + # Reported on the model_response, which is why `error` was added to that + # event: a failed LLM call otherwise paints the row red but not the span. + self._model_end( + event, + content=None, + stop_reason="error", + error=_text(getattr(event, "error", None)) or "llm call failed", + extra={}, + ) + + def _model_end(self, event, *, content, stop_reason, error, extra) -> None: + tracker = self._tracker + if tracker is None: + return + call_id = getattr(event, "call_id", None) + leaf = self._pop_leaf(event.started_event_id, ("llm", call_id)) + if leaf is None: + return + stream = self._pop_stream(call_id) + fields = dict(extra) + if stream is not None: + chunks, first_ts = stream + fields.update( + fw_fields( + streamed=True, + chunks=chunks, + ttft_ms=ms(first_ts - leaf.started) if first_ts is not None else None, + ) + ) + # duration_ms is NOT guarded on model_response, and `durationOf` prefers + # the closing event's value over end-start — which is what keeps model + # durations honest even when the dashboard's FIFO pairing brackets the + # wrong pair. It must be an int: the server's JSON parser drops floats + # and would silently NULL the column. + tracker.emit( + "model_response", + leaf.key, + parent_key=leaf.parent_key, + model=getattr(event, "model", None) or leaf.fields["model"], + request_id=leaf.fields["request_id"], + content=content, + stop_reason=stop_reason, + error=error, + duration_ms=ms(event.timestamp - leaf.started), + **fields, + ) + + @safe + def on_llm_chunk(self, source, event) -> None: + """Emits nothing. Counts chunks and stamps time-to-first-token, once.""" + key = getattr(event, "call_id", None) or getattr(event, "parent_event_id", None) + if key is None: + return + with self._lock: + entry = self._streams.get(key) + if entry is None: + while len(self._streams) >= _MAX_STREAMS: + self._streams.pop(next(iter(self._streams)), None) + self._streams[key] = [1, event.timestamp] + else: + entry[0] += 1 + + def _pop_stream(self, call_id): + if call_id is None: + return None + with self._lock: + entry = self._streams.pop(call_id, None) + return (entry[0], entry[1]) if entry else None + + +# --------------------------------------------------------------------------- +# The listener +# --------------------------------------------------------------------------- + +def event_class(name: str): + """The event class `name`, or None. + + TWO namespaces, because crewai has two and neither is complete. + `crewai.events.event_types` is the flat re-export the bulk of the table + resolves against, but the flow events — `HumanFeedbackRequestedEvent` among + them — are NOT in it; they are reachable only through `crewai.events`, whose + module-level `__getattr__` imports them lazily from + `crewai.events.types.flow_events`. + + Resolving against `event_types` alone is not an error you would see: the + lookup returns None, `probe()` disables that one hook, and the adapter + carries on recording everything else. The HITL events were mapped and + silently never registered until this existed. `getattr`, never `dir()` — + a lazy re-export does not appear in `dir()`. + """ + return getattr(_ct, name, None) or getattr(_ce, name, None) + + +class FailproofAICrewListener(BaseEventListener): + """Registers one handler per event class on the module-level bus. + + `BaseEventListener.__init__` is what performs the registration (it calls + `setup_listeners` with the `crewai_event_bus` singleton), so constructing + the instance *is* the install step — and the instance has to be kept alive + by the adapter or it is garbage collected and silently stops working. + + Every handler is `async def` for the ordering reason in the module + docstring, and does nothing but call a `safe`-wrapped translator, so a bug + in this file can never take down the customer's crew. + """ + + def __init__(self, adapter: _CrewAIAdapter) -> None: + self._adapter = adapter + self._registered: list[tuple[type, Any]] = [] + super().__init__() + + def setup_listeners(self, crewai_event_bus) -> None: + for class_name, method_name in TABLE: + klass = event_class(class_name) + if klass is None: + # Tier 3: one missing event class disables one hook, never the + # whole adapter — the other 90% of the events are still correct. + _compat.probe(NAME, class_name, lambda: False) + continue + handler = self._make_handler(getattr(self._adapter, method_name), method_name) + crewai_event_bus.register_handler(klass, handler) + self._registered.append((klass, handler)) + + @staticmethod + def _make_handler(translator, method_name): + async def _failproofai_handler(source, event): + translator(source, event) + + _failproofai_handler.__name__ = f"failproofai_{method_name}" + _failproofai_handler.__qualname__ = f"failproofai_sdk.crewai.{method_name}" + return _failproofai_handler + + def teardown(self) -> None: + """`crewai_event_bus.off()` for everything `setup_listeners` added.""" + for event_class, handler in self._registered: + try: + crewai_event_bus.off(event_class, handler) + except Exception: # pragma: no cover - teardown must never raise + logger.warning( + "failproofai_sdk: could not unregister the crewai handler for %s", + getattr(event_class, "__name__", event_class), + exc_info=True, + ) + self._registered.clear() + + def handlers(self) -> tuple[tuple[type, Any], ...]: + """(event class, handler) pairs currently registered. For tests.""" + return tuple(self._registered) + + +# --------------------------------------------------------------------------- +# Payload helpers +# --------------------------------------------------------------------------- + +def _text(value) -> "str | None": + if value is None: + return None + if isinstance(value, str): + return truncate(value) + raw = getattr(value, "raw", None) + return truncate(raw if isinstance(raw, str) else str(value)) + + +def _first_text(mapping) -> "str | None": + """A goal string for a crew or flow, from whatever inputs it was given.""" + if isinstance(mapping, dict): + for value in mapping.values(): + if isinstance(value, str) and value.strip(): + return truncate(value) + return None + + +def _dictify(value) -> "dict | None": + """`input=` is declared `dict | None`; a bare string would break the shape. + + CrewAI hands `tool_args` over as the raw JSON string the model produced, so + it is decoded when it decodes: an object in the payload is queryable with + `payload_key_expr`, a JSON-string-inside-a-string is not. + """ + if value is None: + return None + if isinstance(value, dict): + return truncate(value) + if isinstance(value, str): + stripped = value.strip() + if stripped.startswith("{"): + try: + decoded = json.loads(stripped) + except ValueError: + decoded = None + if isinstance(decoded, dict): + return truncate(decoded) + return {"input": truncate(value)} + return {"input": truncate(str(value))} + + +def _listify(value) -> "list | None": + if value is None: + return None + return truncate(list(value) if isinstance(value, (list, tuple)) else [value]) + + +def _messages(value) -> "list | None": + """`messages=` is declared `list[dict]`; CrewAI also allows a bare string.""" + if value is None: + return None + if isinstance(value, str): + return [{"role": "user", "content": truncate(value)}] + return _listify(value) + + +def _tool_names(tools) -> "list | None": + if not tools: + return None + return [getattr(tool, "name", None) or type(tool).__name__ for tool in tools] + + +def _enum_value(value): + return getattr(value, "value", value) if value is not None else None + + +def _hook_name_of(event) -> "str | None": + return ( + getattr(event, "method_name", None) + or getattr(event, "guardrail_name", None) + or "guardrail" + ) + + +# The usage dict is whatever the provider returned, so both spellings are in the +# wild and neither is guaranteed. Reading only one of them reports zero tokens +# for half the providers, at HTTP 200, forever. +_INPUT_KEYS = ("prompt_tokens", "input_tokens", "promptTokens", "inputTokens") +_OUTPUT_KEYS = ("completion_tokens", "output_tokens", "completionTokens", "outputTokens") +_TOTAL_KEYS = ("total_tokens", "totalTokens") + + +def _tokens(usage): + """(input_tokens, output_tokens, normalized_usage_dict).""" + if not isinstance(usage, dict): + return None, None, {} + input_tokens = _count(usage, _INPUT_KEYS) + output_tokens = _count(usage, _OUTPUT_KEYS) + total = _count(usage, _TOTAL_KEYS) + if total is None and (input_tokens is not None or output_tokens is not None): + total = (input_tokens or 0) + (output_tokens or 0) + normalized = {} + for key, value in ( + ("input_tokens", input_tokens), + ("output_tokens", output_tokens), + ("total_tokens", total), + ): + if value is not None: + normalized[key] = value + return input_tokens, output_tokens, normalized + + +def _count(usage, keys): + for key in keys: + value = usage.get(key) + if isinstance(value, bool): + continue + if isinstance(value, int): + return value + if isinstance(value, float): + return int(value) + if isinstance(value, str) and value.isdigit(): + return int(value) + return None + + +adapter = _CrewAIAdapter() diff --git a/sdk/python/failproofai_sdk/integrations/langchain.py b/sdk/python/failproofai_sdk/integrations/langchain.py new file mode 100644 index 000000000..c7ff5ee8d --- /dev/null +++ b/sdk/python/failproofai_sdk/integrations/langchain.py @@ -0,0 +1,1988 @@ +"""LangChain + LangGraph adapter. + +Written against **langchain-core 1.5.2** and **langgraph 1.2.10** (2026-07-29), +and every claim below was read out of those installed packages rather than +recalled. Where this file disagrees with the LangChain docs or with another +vendor's integration, the disagreement is deliberate and the reason is in the +comment next to it. + +How it attaches +--------------- +`langchain_core.tracers.context.register_configure_hook` is public, documented, +and survived the 0.x -> 1.x rewrite. `CallbackManager.configure` injects the +handler into **every** callback manager it builds, so no call site changes:: + + failproofai_sdk.instrument("langchain") + graph.invoke(...) # already recorded + +Four consequences of that hook shape drive the code: + +* with the ``env_var`` form a **fresh handler is constructed per callback + manager** — many times per run — so ``FailproofAITracer.__init__`` is zero-arg + and cheap and **all** cross-callback state lives in the module-level + ``_STATE``; +* ``inheritable=True`` is required or child runs never see it; +* there is **no deregister API** (``_configure_hooks`` is append-only and + private), so ``uninstall()`` clears the ContextVar and unsets the env var; +* we do **not** patch ``BaseCallbackManager.__init__``. OpenInference and + Traceloop do, and ``BaseCallbackManager.merge()`` builds a new manager with + handlers already passed in, so their ``isinstance`` dedup misses and the + handler is added twice. That is a real duplicate-event bug; MLflow patches + ``merge`` as well to work around it. The configure hook has no such hole. + +Why `BaseTracer` +---------------- +`BaseTracer` assembles the run tree and hands over `Run` objects with inputs, +outputs, metadata and timings already collected — two override points instead of +twenty hand-correlated callbacks. `langchain_core.tracers.schemas.Run` *is* +`langsmith.RunTree`. We also subclass `langgraph.callbacks.GraphCallbackHandler` +(under try/except: it is new in langgraph 1.2) for first-class interrupt/resume. + +`run_inline = True` is not optional. `AsyncCallbackManager` dispatches sync +handlers through `run_in_executor` unless a handler sets it, and that hop can +**reorder callbacks** — which scrambles timestamp order and breaks every pairing +in this file. `writer.submit()` is a `deque.append`, so inline on the event loop +is safe. `raise_error` is normally False: LangChain already firewalls handler +exceptions in `handle_event`, and so does `_core.safe`. It follows +`FAILPROOFAI_SDK_STRICT`, because that same firewall otherwise swallows the exception +`safe()` re-raises under strict and the escape hatch does nothing here. + +The mapping +----------- +=========================== ========================================== +LangChain / LangGraph Failproof AI +=========================== ========================================== +root run (no parent) ``agent_start`` / ``agent_end`` +LangGraph node ``hook_triggered`` / ``hook_completed`` +compiled subgraph nested ``agent_start`` (``root/node``) +tool run ``tool_use`` / ``tool_result`` +retriever run ``tool_use`` / ``tool_result`` (summarised) +chat model / LLM run ``model_request`` / ``model_response`` +``interrupt()`` ``human_wait`` + ``agent_pause`` +``Command(resume=...)`` ``agent_resume`` + ``human_input`` +intermediate chains *nothing* (see ``include_chains``) +=========================== ========================================== + +**A LangGraph node is a hook, not a nested agent.** `agent_id` is a +`LowCardinality(String)` column and the primary facet on every dashboard +surface, and `agent_sessions.agent_id = any(...)` returns the first `agent_id` +by time — so promoting `retrieve`, `grade_documents` and `should_continue` to +agents would both drown the facet and label the session with a random node. +Hook spans render structurally identically, and `/hooks` becomes a per-node +latency page for free. + +Corrections to received wisdom, both verified here +-------------------------------------------------- +1. **`thread_id` IS available to callbacks** on this stack. langchain-core's + `ensure_config` stopped promoting `configurable` into `metadata`, which is + what every "thread_id is None" report is about — but langgraph 1.2 re-adds it + in `langgraph._internal._config` via ``_PROPAGATE_TO_METADATA`` = + {thread_id, checkpoint_id, checkpoint_ns, task_id, run_id, assistant_id, + graph_id}. So `metadata["thread_id"]` is populated and is a good default + session key. It is still only the *fourth* resolution step, because a + `thread_id` is a conversation, not necessarily a run. +2. **`GraphCallbackHandler.on_interrupt` does NOT fire for a handler installed + through `register_configure_hook`.** `Pregel.stream` builds the lifecycle + manager with `get_sync_graph_callback_manager_for_config(config)`, which + reads the **raw** ``config["callbacks"]`` — the configure hooks never touch + it — and then gates the whole feature on + ``has_graph_lifecycle_callbacks=bool(manager.handlers)``. So we wrap that + factory (see `_install_graph_callbacks`) to attach the handler to the manager + it returns. If the wrap does not apply, the exception-path fallback below + still produces the full HITL pair; only the resume event needs the wrap, and + that has its own fallback too. + +Control flow is not failure +--------------------------- +LangGraph's runnable does ``except BaseException as e: run_manager +.on_chain_error(e); raise`` with no special case for interrupts, so **every** +HITL pause arrives as an error callback. Reporting it would paint a red error +plus ``agent_end(outcome="failed")`` on every human approval. Any +`langgraph.errors.GraphBubbleUp` subclass — `GraphInterrupt`, `NodeInterrupt`, +`ParentCommand`, `GraphDrained` — is therefore treated as control flow. +""" + +import contextvars +import dataclasses +import logging +import os +import threading +from datetime import datetime, timezone +from typing import Any, Iterable + +from failproofai_sdk import _context +from failproofai_sdk.integrations import _compat, _core +from failproofai_sdk.integrations._core import ( + Patcher, + RunTracker, + framework_fields, + fw_fields, + ms, + normalize_agent_id, + safe, + truncate, +) + +logger = logging.getLogger("failproofai_sdk.integrations") + +NAME = "langchain" +MODULE = "langchain_core" +DIST = "langchain-core" +EXTRA = "langchain" +ENV_VAR = "FAILPROOFAI_SDK_TRACE_LANGCHAIN" + +# The documented escape hatch for session stitching: +# graph.invoke(x, config={"metadata": {"failproofai_sdk_session_id": sid}}) +SESSION_METADATA_KEY = "failproofai_sdk_session_id" + +# Checked in order after the explicit key above. `thread_id` is last because a +# thread is a *conversation*; two turns on one thread are two runs, and a user +# who wants them merged has said so with one of the earlier keys. +SESSION_METADATA_FALLBACKS = ("session_id", "conversation_id", "thread_id") + +# LangSmith's convention for "machinery, not user-visible work". We demote these +# rather than dropping them: they never become a span, but they stay in the +# parent chain so their children still find the agent above them. +HIDDEN_TAG = "langsmith:hidden" + +_FIELD_LIMIT = 2048 # inputs/outputs are graph state; the per-event budget is not the only guard + + +# --------------------------------------------------------------------------- +# Framework imports +# --------------------------------------------------------------------------- +# This module is only ever imported by `instrument("langchain")`, so importing +# the framework at module scope is fine — `import failproofai_sdk` never gets here. +# `require_module` turns a missing install into an ImportError carrying the +# literal install command. +_compat.require_module(MODULE, dist=DIST, extra=EXTRA) + +from langchain_core.tracers.base import BaseTracer # noqa: E402 +from langchain_core.tracers.context import register_configure_hook # noqa: E402 + +try: # langgraph >= 1.2 only + from langgraph.callbacks import GraphCallbackHandler as _GraphCallbackHandler +except ImportError: # pragma: no cover - exercised on langgraph < 1.2 / absent + _GraphCallbackHandler = None + +try: + from langgraph.errors import GraphBubbleUp as _GraphBubbleUp +except ImportError: # pragma: no cover + _GraphBubbleUp = None + +try: # `Command` tells a resume from a fresh turn; `Interrupt` derives a pause id + from langgraph.types import Command as _Command + from langgraph.types import Interrupt as _Interrupt +except ImportError: # pragma: no cover + _Command = None + _Interrupt = None + +# Name-based fallback for the case where `langgraph.errors` moved. Getting this +# wrong is expensive and silent (a red error on every human approval), so it is +# worth a belt-and-braces check rather than a bare `isinstance`. +_CONTROL_FLOW_NAMES = frozenset( + {"GraphBubbleUp", "GraphInterrupt", "NodeInterrupt", "ParentCommand", "GraphDrained"} +) + + +def _is_control_flow(exc: BaseException | None) -> bool: + if exc is None: + return False + if _GraphBubbleUp is not None and isinstance(exc, _GraphBubbleUp): + return True + return any(cls.__name__ in _CONTROL_FLOW_NAMES for cls in type(exc).__mro__) + + +if _GraphCallbackHandler is not None: + _GraphBase: Any = _GraphCallbackHandler +else: # pragma: no cover - only on langgraph < 1.2 + + class _GraphBase: # type: ignore[no-redef] + """Stand-in so the two lifecycle overrides always have a home. + + Deliberately **not** registered anywhere: langgraph's dispatch is an + `isinstance(h, GraphCallbackHandler)` filter, so on an older langgraph + these methods are simply never called, which is the correct behaviour. + """ + + def on_interrupt(self, event: Any) -> Any: ... + + def on_resume(self, event: Any) -> Any: ... + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +# --------------------------------------------------------------------------- +# State +# --------------------------------------------------------------------------- + +@dataclasses.dataclass +class _Options: + """Everything `instrument("langchain", ...)` accepts.""" + + session_id: str | None = None + include_chains: frozenset = frozenset() + capture_content: bool = True + graph_callbacks: bool = True + + +@dataclasses.dataclass +class _Session: + """One Failproof AI session, which may outlive a single `.invoke()`. + + It has to: a human-in-the-loop graph runs `invoke()`, interrupts, and is + resumed by a *second* `invoke()` minutes later. Both are the same session + and the same root agent, and the agent stays open across the gap so that + `agent_pause` -> `agent_resume` measures the wait (which is the only thing + that feeds the dashboard's `pausedMs`). + """ + + session_id: str + agent_key: str + agent_id: str + open_pauses: dict = dataclasses.field(default_factory=dict) + reported_error: bool = False + + +@dataclasses.dataclass +class _RemoteResume: + """Bookkeeping for a resume whose pause was opened in ANOTHER process. + + Set on the ROOT `_RunInfo` only, and only when this process has no open + pause of its own to close — i.e. exactly the deployment shape where the + interrupt was served by one worker and the approval by another. See + `_close_remote_pause` for how the pause id is recovered. + """ + + value: Any = None + # checkpoint-ns tuple -> the `langgraph_step` of the first node seen at that + # level. Only that first superstep re-runs interrupted tasks; everything + # after it is ordinary downstream work. + levels: dict = dataclasses.field(default_factory=dict) + # The deepest level langgraph has told us is resuming. A subgraph host node + # sits at a shallower level than the task that actually interrupted. + deepest: tuple | None = None + done: set = dataclasses.field(default_factory=set) + + +@dataclasses.dataclass +class _RunInfo: + """What we need about a LangChain run after its start callback returns.""" + + id: str + parent: str | None + name: str + run_type: str + started: datetime = dataclasses.field(default_factory=_now) + hidden: bool = False + kind: str = "" # "" | "root" | "node" | "tool" | "retriever" | "model" | "chain" + # Set only on a ROOT run that is itself a leaf — a bare `llm.invoke()`, a + # standalone tool. Such a run is both the session's agent and a model/tool + # call, and it needs both pairs. See `_start_root`. + leaf_kind: str = "" + root: str | None = None + session: _Session | None = None + node: str | None = None + tool_call_id: str | None = None + model: str | None = None + messages: list | None = None + ttft_ms: int | None = None + chunks: int = 0 + remote: _RemoteResume | None = None # root only + + +class _State: + """All cross-callback state, module level on purpose. + + The `env_var` form of `register_configure_hook` constructs a **new** + `FailproofAITracer` per callback manager, so anything kept on `self` would be + lost between a start and its end. Nothing here touches contextvars either: + `ContextVar.reset(token)` raises across asyncio tasks as well as threads, so + a surface whose start and end are separate calls can never hold a token. + """ + + MAX_RUNS = 10_000 + MAX_SESSIONS = 1_000 + + def __init__(self) -> None: + # RLock because a handler body can re-enter through a nested helper. + self.lock = threading.RLock() + # The kill switch `uninstall()` needs and neither of its two other + # levers actually provides. A configure hook cannot be deregistered, so + # removal is "make the hook produce nothing" — but clearing the + # ContextVar only reaches contexts derived from the one calling + # `uninstall()`, and unsetting the env var is skipped whenever the + # process already had it set (`self._set_env` is False then, so we do + # not clobber somebody else's environment). Either hole leaves + # `_configure` constructing a live zero-arg tracer per callback manager + # — VERIFIED: with FAILPROOFAI_SDK_TRACE_LANGCHAIN=1 exported before + # `instrument()`, every event was still recorded after `uninstrument()`. + # Checked at the two entry points that gate everything else: no + # `_RunInfo` is registered, so `_on_end`, `_count_token`, + # `_on_interrupt` and `_on_resume` all fall out on their own lookups. + self.enabled = False + self.options = _Options() + self.tracker = RunTracker(NAME, base_fields=_base_fields()) + self.runs: dict[str, _RunInfo] = {} + self.sessions: dict[str, _Session] = {} + # run_id -> the LLMResult / exception stashed by a public callback for + # the `_end_trace` that follows it. + self.responses: dict[str, Any] = {} + self.errors: dict[str, BaseException] = {} + + def reset(self) -> None: + with self.lock: + self.tracker.reset() + self.runs.clear() + self.sessions.clear() + self.responses.clear() + self.errors.clear() + + def evict(self) -> None: + """Caller holds the lock. FIFO; dicts keep insertion order. + + Orphaned entries are normal, not exceptional: a cancelled stream, a + crashed node, a framework that skipped an end callback. Unbounded, each + of these dicts is a memory leak in a long-lived server. + """ + for table, cap in ( + (self.runs, self.MAX_RUNS), + (self.sessions, self.MAX_SESSIONS), + (self.responses, self.MAX_RUNS), + (self.errors, self.MAX_RUNS), + ): + while len(table) >= cap: + table.pop(next(iter(table)), None) + + +def _base_fields() -> dict: + fields = framework_fields(NAME, DIST) + version = _compat.version_string("langgraph") + if version: + fields["fw_langgraph_version"] = version + return fields + + +_STATE = _State() + + +# --------------------------------------------------------------------------- +# Small readers over the Run object +# --------------------------------------------------------------------------- + +def _meta(run: Any) -> dict: + meta = getattr(run, "metadata", None) + return dict(meta) if isinstance(meta, dict) else {} + + +def _extra(run: Any) -> dict: + extra = getattr(run, "extra", None) + return extra if isinstance(extra, dict) else {} + + +def _tags(run: Any) -> list: + tags = getattr(run, "tags", None) + return list(tags) if tags else [] + + +def _shrink(value: Any) -> Any: + """Payload discipline for the big three: inputs, outputs, graph state.""" + if not _STATE.options.capture_content: + return None + return truncate(value, _FIELD_LIMIT) + + +# A run of one of these types is a leaf — a model call, a tool call, a +# retrieval. It is never the LangGraph node's *own* run. Verified against +# langgraph 1.2.11: whatever you hand `add_node` (a function, a Runnable, a +# `BaseTool`, a compiled subgraph), the node's own run is always a **chain** +# run tagged `graph:step:N`, and the thing you passed runs as a child of it. +_LEAF_RUN_TYPES = frozenset({"llm", "chat_model", "tool", "retriever"}) + +# LangChain tags every step of a `RunnableSequence` `seq:step:N`. LangGraph tags +# a node's own run `graph:step:N`. Only the second is a node. +_INNER_STEP_TAG = "seq:step:" + + +def _node_of(run: Any, meta: dict) -> str | None: + """The LangGraph node name **iff this run is the node's own run**. + + Every inner Runnable inherits `langgraph_node` from the node that contains + it, so the metadata alone matches the node, the chat model inside it, the + tool it called and each conditional-edge function. `run.name` is exactly the + `kwargs["name"] or serialized["name"]` the callback was given, and only the + node's own run has it equal to `langgraph_node`. Verified against + langgraph 1.2.10: an inner `cond`/`fan` edge function reports + `name="cond"` with `langgraph_node="act"` and is correctly excluded. + + The name alone is not enough, because **the name is the user's to choose on + both sides**. Two collisions were verified on langgraph 1.2.11, and each one + silently deleted the most valuable event in the trace: + + * ``add_node("lookup_population", ToolNode([lookup_population]))`` — naming a + node after the tool it runs, which is the obvious thing to do — made the + *tool's* run match too. It was recorded as a second node visit, so + `tool_use`/`tool_result` were never emitted: the arguments, the result and + the LLM's `tool_call_id` all vanished, and `/tools` showed the call had + never happened. + * ``add_node("ChatOpenAI", ...)`` did the same to the chat model run: + no `model_request`/`model_response`, so the model name, both token counts + and the latency were dropped while the trace still looked populated. + * an inner Runnable carrying `run_name` equal to the node key produced + **two** `hook_triggered`/`hook_completed` pairs for one node visit, which + doubles that node's visit count and halves its apparent latency. + + So the run must also be shaped like a node's own run: a non-leaf run type, + and not an inner step of a `RunnableSequence`. Both are *exclusions* — if + langgraph ever stops emitting `seq:step:` tags this degrades to the old + duplicate span rather than to no spans at all, which is the safe direction + for a check that gates `hook_triggered` **and** `_ensure_subgraph_agent`. + """ + node = meta.get("langgraph_node") + if not node or node != (getattr(run, "name", None) or ""): + return None + if str(getattr(run, "run_type", "") or "") in _LEAF_RUN_TYPES: + return None + if any(str(tag).startswith(_INNER_STEP_TAG) for tag in _tags(run)): + return None + return str(node) + + +def _ns_parts(meta: dict) -> list: + """`langgraph_checkpoint_ns` split into its `name:uuid` segments. + + For a top-level node this is one segment (`plan:uuid`); for a node inside a + compiled subgraph it is `child:uuid|sub_step:uuid`. The number of segments + beyond the first is the subgraph nesting depth, and the leading segments + name the subgraphs — which is how nested agents get their ids without + having to recognise a compiled `Pregel` from a `Run` object. + """ + ns = meta.get("langgraph_checkpoint_ns") + if not ns or not isinstance(ns, str): + return [] + return ns.split("|") + + +# --------------------------------------------------------------------------- +# Session resolution +# --------------------------------------------------------------------------- + +def _resolve_session_id(run: Any, meta: dict) -> str: + """Pick the session id for a root run. + + In order: + + 1. ``instrument("langchain", session_id=...)`` — an explicit override wins. + 2. ``config={"metadata": {"failproofai_sdk_session_id": ...}}`` — the documented + per-call key. + 3. the ambient `failproofai_sdk.session()` / `failproofai_sdk.agent()` scope, so a + hand-written outer bracket and the adapter produce **one** session. + 4. ``metadata["session_id" | "conversation_id" | "thread_id"]``. + 5. the root run id. + + Never synthesised from scratch: a made-up id splits one run into many + sessions, which is a silent wrong answer rather than a loud one. + """ + if _STATE.options.session_id: + return str(_STATE.options.session_id) + explicit = meta.get(SESSION_METADATA_KEY) + if explicit: + return str(explicit) + ambient = _context.session_id() + if ambient: + return ambient + for key in SESSION_METADATA_FALLBACKS: + value = meta.get(key) + if value: + return str(value) + return str(getattr(run, "id", "")) or _context.DEFAULT_AGENT_ID + + +# --------------------------------------------------------------------------- +# Emission helpers +# --------------------------------------------------------------------------- + +def _emit(method: str, info: _RunInfo, **fields: Any) -> None: + _STATE.tracker.emit(method, info.id, parent_key=info.parent, **fields) + + +def _emit_on_agent(session: _Session, method: str, **fields: Any) -> None: + _STATE.tracker.emit(method, session.agent_key, **fields) + + +def _fw_common(run: Any, info: _RunInfo, meta: dict) -> dict: + """The `fw_*` extras every event from this adapter carries. + + Namespaced, and that is a **safety** rule rather than a style one: + `_schema._build()` merges extra fields last, so an extra called `tool_name`, + `model` or `outcome` silently overwrites the declared field and therefore + the promoted column. `_core.guard_extras` is the backstop; + `fw_fields` is how we stay away from the edge. + """ + return fw_fields( + run_id=info.id, + parent_run_id=info.parent, + node=info.node or meta.get("langgraph_node"), + step=meta.get("langgraph_step"), + checkpoint_ns=meta.get("langgraph_checkpoint_ns"), + thread_id=meta.get("thread_id"), + tags=_tags(run) or None, + hidden=True if info.hidden else None, + ) + + +# --------------------------------------------------------------------------- +# Start +# --------------------------------------------------------------------------- + +def _on_start(run: Any) -> None: + state = _STATE + if not state.enabled: + return + rid = str(run.id) + parent = str(run.parent_run_id) if getattr(run, "parent_run_id", None) else None + meta = _meta(run) + + with state.lock: + state.evict() + info = _RunInfo( + id=rid, + parent=parent, + name=str(getattr(run, "name", "") or ""), + run_type=str(getattr(run, "run_type", "") or ""), + hidden=HIDDEN_TAG in _tags(run), + ) + state.runs[rid] = info + # Every run is linked, span or not. This is what lets a tool three + # Runnables deep still find the agent above it: `RunTracker.identity` + # walks the link chain, and an intermediate chain that emits nothing + # would otherwise break the walk. + state.tracker.link(rid, parent) + + if parent is None: + _start_root(run, info, meta) + return + + holder = state.runs.get(parent) + info.root = holder.root if holder is not None else None + info.session = holder.session if holder is not None else None + + node = _node_of(run, meta) + if node is not None: + info.kind = "node" + info.node = node + _start_node(run, info, meta) + return + + if info.run_type in ("llm", "chat_model"): + info.kind = "model" + _start_model(run, info, meta) + return + if info.run_type == "tool": + info.kind = "tool" + _start_tool(run, info, meta) + return + if info.run_type == "retriever": + info.kind = "retriever" + _start_retriever(run, info, meta) + return + + # Everything else — RunnableSequence, prompt templates, output parsers, + # conditional-edge functions, the compiled-subgraph Pregel run itself. + # Emitting these would bury the timeline under machinery, so they are + # linked and otherwise invisible unless explicitly allowlisted. + if info.name and info.name in state.options.include_chains and not info.hidden: + info.kind = "chain" + _emit( + "hook_triggered", + info, + hook_name=info.name, + hook_id=info.id, + trigger_event="pipeline", + input=_shrink(getattr(run, "inputs", None)), + **_fw_common(run, info, meta), + ) + + +def _start_root(run: Any, info: _RunInfo, meta: dict) -> None: + """The root run becomes the session's agent — and its **first** event. + + `agent_sessions.agent_id = any(...)` resolves to the first `agent_id` by + time over `ORDER BY (session_id, ts, ...)`, so anything emitted before this + would name the session after a node. The dashboard also parents every leaf + to the open agent with the same `agent_id` and **synthesises a + never-ending root span** when there is none, so this must not be skipped. + """ + state = _STATE + info.kind = "root" + info.root = info.id + session_id = _resolve_session_id(run, meta) + + existing = state.sessions.get(session_id) + if ( + existing is not None + and existing.open_pauses + and existing.agent_key in state.tracker.open_agents() + and _is_continuation(run) + ): + # A resume: the previous `.invoke()` interrupted, we deliberately did + # not close its agent, and this is the continuation. Reuse the identity + # instead of opening a second root span for the same logical run. + # + # `open_pauses` is the whole test, and leaving it out was a silent + # data-loss bug rather than a cosmetic one. "The session's agent is + # still open" is ALSO true of two roots that merely OVERLAP IN TIME + # under one session id — `.batch()` (langchain-core opens one root run + # per input), a top-level `RunnableParallel` of chains, or two web + # requests carrying the same conversation id. Those were read as + # resumes: the second root got no `agent_start` at all, its work was + # relabelled with the first root's `agent_id`, the first root to finish + # closed the shared agent, and every event the other root emitted after + # that resolved to nothing and was DROPPED — a real model call, with + # its tokens and its latency, gone with one "could not resolve a + # session" line at WARNING. A run that is genuinely paused always has an + # open pause: `_end_root` returns without `agent_end` exactly when + # `session.open_pauses` is non-empty, which is the only way the agent + # stays open past its root, and `_suspend` is the only thing that fills + # it. So this distinguishes the two cases precisely. + # + # `_is_continuation` is the second half of that test and it is not + # redundant: an open pause bounds how long the window lasts, but it + # does not close it. A HITL turn can sit paused on a human for + # **minutes**, and any other run that happens to carry the same session + # id during that window — a second web request on one conversation id, + # a background summariser, a different graph entirely — was read as the + # approval. VERIFIED against langgraph 1.2.11: the second run got no + # `agent_start`, its nodes were folded into the paused run's span, and + # the adapter emitted `agent_resume` + `human_input` for a human who + # had answered nothing — `human_input.response` empty, the pause closed, + # and the paused run's `agent_end` reporting `success`. On a product + # whose whole job is to gate an action on human approval, fabricating + # the approval is the worst wrong answer available. LangGraph only ever + # continues an interrupted thread through `Command(...)` or a `None` + # input; a fresh state dict is a NEW turn, not an answer. + info.session = existing + state.tracker.link(info.id, existing.agent_key) + _resume(existing, run) + return + + identity = state.tracker.start_agent( + info.id, + agent_id=normalize_agent_id(info.name, "agent"), + session_id=session_id, + goal=_goal_of(run), + **_fw_common(run, info, meta), + ) + session = _Session( + session_id=identity.session_id or session_id, + agent_key=info.id, + agent_id=identity.agent_id or "agent", + ) + info.session = session + state.sessions[session.session_id] = session + + # This is a resume, but nothing in THIS process is paused — so the pause was + # opened somewhere else. That is not an edge case, it is the deployment + # shape: one worker serves the request that interrupts, a human answers + # minutes later, and whichever worker picks up that request resumes against + # the shared checkpointer. Before this, such a resume emitted no + # `agent_resume` and no `human_input` at all, so the `human_wait` and + # `agent_pause` from the first process stayed open FOREVER — every + # cross-process approval left its session reporting "still waiting on a + # human" after the human had answered, and `pausedMs` never closed. + # `_close_remote_pause` recovers the id the other process used. + answer = _resume_values(run) + if answer is not None: + info.remote = _RemoteResume(value=answer) + + # A root run that is ITSELF a leaf still has to be recorded as one. + # + # `ChatOpenAI(...).invoke(...)` outside any graph is a single run with no + # parent and `run_type="chat_model"`. Handled only as a root it produced an + # `agent_start`/`agent_end` pair and NOTHING ELSE — no `model_request`, no + # `model_response`, so the model name, both token counts and the latency of + # a direct model call were dropped on the floor, silently, while the trace + # still looked populated. Direct `.invoke()` is not an edge case: a + # classifier, a summariser, a one-shot rewrite are all shaped like this. + # + # The agent span stays (the dashboard parents leaves to an open agent with + # the same `agent_id` and synthesises a never-ending root span when there is + # none), so this is purely additive: the same run now emits its leaf pair + # INSIDE its own agent span. + starter = _ROOT_LEAF_STARTERS.get(info.run_type) + if starter is not None: + info.leaf_kind = _LEAF_KIND_OF[info.run_type] + starter(run, info, meta) + + +def _goal_of(run: Any) -> str | None: + inputs = getattr(run, "inputs", None) + if not _STATE.options.capture_content or inputs is None: + return None + if isinstance(inputs, dict): + messages = inputs.get("messages") + if isinstance(messages, (list, tuple)) and messages: + content = getattr(messages[-1], "content", None) + if isinstance(content, str) and content: + return truncate(content, 512) + return truncate(str(inputs), 512) + + +def _start_node(run: Any, info: _RunInfo, meta: dict) -> None: + """A LangGraph node -> `hook_triggered`. + + Also the point at which a compiled **subgraph** becomes a nested agent: a + node whose checkpoint namespace is more than one segment deep is running + inside one, and its parent run *is* the subgraph's Pregel run (verified on + langgraph 1.2.10). Deriving it here means we never have to recognise a + `Pregel` from a `Run`, and it nests to arbitrary depth for free. + """ + parts = _ns_parts(meta) + if len(parts) > 1 and info.parent is not None: + _ensure_subgraph_agent(info, parts[:-1]) + + remote = _remote_of(info) + if remote is not None: + # First node seen at this level wins: langgraph re-runs the interrupted + # tasks in the level's first superstep and nothing else (VERIFIED on + # 1.2.11 — a sibling that had already succeeded in the same superstep + # does NOT re-run), so anything at a later step is downstream work. + remote.levels.setdefault(tuple(parts[:-1]), meta.get("langgraph_step")) + + if info.hidden: + return + _emit( + "hook_triggered", + info, + hook_name=info.node, + hook_id=info.id, + trigger_event="graph_node", + input=_shrink(getattr(run, "inputs", None)), + **_fw_common(run, info, meta), + ) + + +def _ensure_subgraph_agent(info: _RunInfo, prefix: list) -> None: + state = _STATE + key = info.parent + if key is None or key in state.tracker.open_agents(): + return + holder = state.runs.get(key) + session = info.session + if holder is None or session is None: + return + names = [part.split(":", 1)[0] for part in prefix if part] + agent_id = "/".join([session.agent_id, *names]) + state.tracker.start_agent( + key, + agent_id=agent_id, + parent_key=holder.parent, + session_id=session.session_id, + **fw_fields(run_id=key, subgraph=names[-1] if names else None, kind="subgraph"), + ) + holder.kind = "subgraph" + holder.session = session + + +def _start_tool(run: Any, info: _RunInfo, meta: dict) -> None: + # The LLM-issued id when there is one, so our events line up with the + # provider's logs and with the `tool_calls` on the assistant message. It + # arrives in the `on_tool_start` kwargs and `_create_tool_run` parks the + # whole kwargs dict in `run.extra`. + info.tool_call_id = str(_extra(run).get("tool_call_id") or info.id) + if info.hidden: + return + _emit( + "tool_use", + info, + tool_name=info.name or "tool", + tool_call_id=info.tool_call_id, + input=_shrink(getattr(run, "inputs", None)), + **_fw_common(run, info, meta), + ) + + +def _start_retriever(run: Any, info: _RunInfo, meta: dict) -> None: + info.tool_call_id = info.id + if info.hidden: + return + inputs = getattr(run, "inputs", None) + query = inputs.get("query") if isinstance(inputs, dict) else inputs + _emit( + "tool_use", + info, + tool_name="retriever:%s" % (info.name or "retriever"), + tool_call_id=info.tool_call_id, + input={"query": truncate(query, _FIELD_LIMIT)} if _STATE.options.capture_content else None, + **_fw_common(run, info, meta), + ) + + +def _start_model(run: Any, info: _RunInfo, meta: dict) -> None: + info.model = _model_name(run, info, meta) + messages = _STATE.responses.pop("messages:" + info.id, None) + if messages is None: + messages = _prompts_as_messages(getattr(run, "inputs", None)) + info.messages = messages + if info.hidden: + return + _emit( + "model_request", + info, + # The correlation id the dashboard's detail panel pairs on. No SDK ever + # set it before, which is why `executionGraph` falls back to FIFO + # pairing per agent_id and concurrent calls mis-pair. + request_id=info.id, + model=info.model, + messages=messages if _STATE.options.capture_content else None, + tools=_tools_of(run), + **_fw_common(run, info, meta), + ) + + +def _model_name(run: Any, info: _RunInfo, meta: dict) -> str: + """`ls_model_name` first, then the invocation params, then the class name. + + `ls_model_name` is the LangSmith standard key and is what a real provider + integration sets. It is **absent** on the fake chat models used in tests and + on some community integrations, so the fallbacks are load-bearing rather + than defensive padding. + """ + name = meta.get("ls_model_name") + if name: + return str(name) + params = _extra(run).get("invocation_params") + if isinstance(params, dict): + for key in ("model_name", "model", "model_id", "deployment_name"): + value = params.get(key) + if value: + return str(value) + return info.name or "unknown" + + +def _tools_of(run: Any) -> list | None: + params = _extra(run).get("invocation_params") + if not isinstance(params, dict): + return None + tools = params.get("tools") + if isinstance(tools, (list, tuple)) and tools: + return truncate(list(tools), _FIELD_LIMIT) + return None + + +def _prompts_as_messages(inputs: Any) -> list | None: + """Text-completion runs arrive as `{"prompts": [...]}`. + + Chat runs are captured from `on_chat_model_start`, where the real + `BaseMessage` objects are still available — `_create_chat_model_run` + flattens them to `"System: ...\\nHuman: ..."` strings before they reach the + `Run`, which would lose the roles. + """ + if not isinstance(inputs, dict): + return None + prompts = inputs.get("prompts") + if isinstance(prompts, (list, tuple)): + return [{"role": "user", "content": truncate(p, _FIELD_LIMIT)} for p in prompts] + return None + + +_ROLES = {"human": "user", "ai": "assistant", "system": "system", "tool": "tool"} + + +def _normalize_messages(batches: Any) -> list | None: + if not batches: + return None + batch = batches[-1] if isinstance(batches[-1], (list, tuple)) else batches + out = [] + for message in batch: + kind = str(getattr(message, "type", "") or "") + entry: dict = { + "role": _ROLES.get(kind, kind or "user"), + "content": truncate(getattr(message, "content", ""), _FIELD_LIMIT), + } + calls = getattr(message, "tool_calls", None) + if calls: + entry["tool_calls"] = truncate(list(calls), _FIELD_LIMIT) + out.append(entry) + return out + + +# A root run whose own `run_type` is one of these is a leaf as well as the +# session's agent. Keyed by LangChain's `run_type` string. +_LEAF_KIND_OF = { + "llm": "model", + "chat_model": "model", + "tool": "tool", + "retriever": "retriever", +} +_ROOT_LEAF_STARTERS = { + "llm": _start_model, + "chat_model": _start_model, + "tool": _start_tool, + "retriever": _start_retriever, +} + + +# --------------------------------------------------------------------------- +# End +# --------------------------------------------------------------------------- + +def _on_end(run: Any) -> None: + state = _STATE + rid = str(run.id) + with state.lock: + info = state.runs.get(rid) + exc = state.errors.pop(rid, None) + response = state.responses.pop(rid, None) + if info is None: + return + meta = _meta(run) + + if info.kind == "root": + # Close the leaf pair first when the root was also a leaf: the + # dashboard closes the agent span at `agent_end`, so a + # `model_response` emitted after it is attributed to nothing. + if info.leaf_kind: + _ROOT_LEAF_ENDERS[info.leaf_kind](run, info, meta, exc, response) + # ...and the leaf pair we just closed OWNS the failure, exactly + # as it does for a nested tool or model run (see the matching + # line at the bottom of this function). Without this, a failing + # `tool.invoke()` or `llm.invoke()` at the top level reported + # the same exception twice — once as `tool_result.error` and + # again as a standalone `error` event — so one failure counted + # as two on `sessionSummary.errorCount`, while the identical + # failure one Runnable deeper counted as one. + if info.session is not None and not _is_control_flow(exc) and ( + exc is not None or getattr(run, "error", None) + ): + info.session.reported_error = True + _end_root(run, info, meta, exc) + return + + state.runs.pop(rid, None) + + if info.kind == "subgraph" or rid in state.tracker.open_agents(): + state.tracker.end_agent( + rid, + outcome=_outcome(run, exc), + summary=_error_text(run, exc), + ) + elif info.hidden: + pass + elif info.kind == "node" or info.kind == "chain": + _end_hook(run, info, meta, exc) + elif info.kind == "tool": + _end_tool(run, info, meta, exc) + elif info.kind == "retriever": + _end_retriever(run, info, meta, exc) + elif info.kind == "model": + _end_model(run, info, meta, exc, response) + + if info.kind == "node": + # Strictly BEFORE the `_suspend` below: a node that answers one + # interrupt and immediately raises the next must close the old pause + # before opening the new one, and on langgraph 1.2 both carry the + # same id (it is derived from the task's namespace, not the call). + _close_remote_pause(info, meta) + + # The exception-path HITL fallback, deliberately outside the span + # handling above so that it still fires for a `langsmith:hidden` node + # and for a subgraph that bubbled the interrupt up. `_suspend` dedups on + # `Interrupt.id`, so this and `on_interrupt` cannot double-emit. + interrupts = _interrupts_of(exc) + if interrupts and info.session is not None: + _suspend(info.session, interrupts) + if exc is not None and not _is_control_flow(exc) and info.session is not None: + # The span that owns this failure has reported it. The root must not + # report it again, or `sessionSummary.errorCount` counts one failure + # twice — once on the leaf and once as a standalone `error` event. + info.session.reported_error = True + + +def _outcome(run: Any, exc: BaseException | None) -> str: + if _is_control_flow(exc): + return "paused" + if exc is not None or getattr(run, "error", None): + return "failed" + return "success" + + +def _error_text(run: Any, exc: BaseException | None) -> str | None: + """The error as a short string, never the whole stacktrace. + + `run.error` is `repr(exc)` plus the formatted traceback, which is a fine + thing to keep on the `traceback` field of an `error` event and a terrible + thing to put in `tool_result.error`, where the dashboard renders it inline. + """ + if _is_control_flow(exc): + return None + if exc is not None: + return truncate("%s: %s" % (type(exc).__name__, exc), _FIELD_LIMIT) + error = getattr(run, "error", None) + if error: + return truncate(str(error).splitlines()[0], _FIELD_LIMIT) + return None + + +def _error_message(run: Any, exc: BaseException | None) -> str | None: + """`_error_text` minus the type prefix, for the `error` event only. + + `error` is the one event that carries `error_type` as its OWN field, and the + server builds the row's `summary` as ``": "``. Feeding + it `_error_text` — which prefixes the type because `tool_result.error` and + `agent_end.summary` have nowhere else to say it — rendered every entry on + the Errors surface as ``ValueError: ValueError: denominator must be + non-zero``. The CrewAI, LlamaIndex and Pydantic AI adapters all pass a bare + `str(exc)` here; this makes the fourth agree with them. + """ + if _is_control_flow(exc): + return None + if exc is not None: + return truncate(str(exc), _FIELD_LIMIT) or type(exc).__name__ + error = getattr(run, "error", None) + if error: + return truncate(str(error).splitlines()[0], _FIELD_LIMIT) + return None + + +def _end_hook(run: Any, info: _RunInfo, meta: dict, exc: BaseException | None) -> None: + _emit( + "hook_completed", + info, + hook_name=info.node or info.name, + hook_id=info.id, + # `"paused"` for a GraphInterrupt: the node did not fail, it stopped to + # ask a human. `"failed"`, never `"failure"` — the server only counts + # error|failed|timeout|rejected. + outcome=_outcome(run, exc), + output=_shrink(getattr(run, "outputs", None)), + error=_error_text(run, exc), + **_fw_common(run, info, meta), + ) + + +def _end_tool(run: Any, info: _RunInfo, meta: dict, exc: BaseException | None) -> None: + outputs = getattr(run, "outputs", None) + output = outputs.get("output") if isinstance(outputs, dict) else outputs + output, failed = _tool_output(output) + _emit( + "tool_result", + info, + tool_name=info.name or "tool", + tool_call_id=info.tool_call_id or info.id, + output=_shrink(output), + error=_error_text(run, exc) or failed, + **_fw_common(run, info, meta), + ) + + +def _tool_output(output: Any) -> tuple: + """The tool's actual result, plus an error string when it failed quietly. + + A tool invoked the way every modern tool loop invokes one — handed the + LLM's `ToolCall` dict rather than a bare argument dict, which is what + `bind_tools` produces and what the docs show — returns a **`ToolMessage`**, + not a string. `truncate` has no JSON shape for one, so it fell back to + `repr` and the single most-read field in a tool loop rendered as + ``ToolMessage(content='37000000', name='lookup_population', tool_call_id=…)`` + instead of ``37000000``. + + `status` is the second half. A `ToolMessage` carries `status="error"` when + the tool failed but the framework converted the exception into a message + for the model instead of raising — `run.error` is empty on that path, so the + failure had NO representation at all: `is_error` 0, a green span, and the + text of the exception sitting in an output field nobody filters on. + """ + if getattr(output, "type", None) != "tool": + return output, None + content = getattr(output, "content", None) + failed = None + if getattr(output, "status", None) == "error": + failed = truncate(content if isinstance(content, str) else str(content), _FIELD_LIMIT) + return content, failed + + +def _end_retriever(run: Any, info: _RunInfo, meta: dict, exc: BaseException | None) -> None: + _emit( + "tool_result", + info, + tool_name="retriever:%s" % (info.name or "retriever"), + tool_call_id=info.tool_call_id or info.id, + output=_summarize_documents(getattr(run, "outputs", None)), + error=_error_text(run, exc), + **_fw_common(run, info, meta), + ) + + +def _summarize_documents(outputs: Any) -> dict | None: + """`{"n": ..., "sources": [...]}` — never the document text. + + A retriever that returns twenty 4KB chunks would otherwise put 80KB of + prose into one event, on every hop of every RAG loop. None of it is a + promoted column, so querying it means `JSONExtract` over the payload, which + has already caused a memory blowup in the events store in this product. + """ + if not isinstance(outputs, dict): + return None + docs = outputs.get("documents") + if not isinstance(docs, (list, tuple)): + return None + sources = [] + for index, doc in enumerate(docs[:10]): + meta = getattr(doc, "metadata", None) or {} + source = meta.get("source") or meta.get("id") or meta.get("file_path") + sources.append(truncate(str(source) if source else "doc[%d]" % index, 256)) + return {"n": len(docs), "sources": sources} + + +def _end_model( + run: Any, info: _RunInfo, meta: dict, exc: BaseException | None, response: Any +) -> None: + usage = _usage(response) + content, role, stop_reason = _completion(response) + if exc is not None: + stop_reason = "error" + extras = _fw_common(run, info, meta) + if info.chunks: + extras.update(fw_fields(streamed=True, chunks=info.chunks, ttft_ms=info.ttft_ms)) + _emit( + "model_response", + info, + request_id=info.id, + model=info.model, + stop_reason=stop_reason, + content=content if _STATE.options.capture_content else None, + role=role, + input_tokens=usage.get("input_tokens") if usage else None, + output_tokens=usage.get("output_tokens") if usage else None, + # Shipped as a dict as well: both `event_summary.rs` and + # `sessionSummary.ts` fall back to `payload.usage` for tokens. + usage=usage or None, + error=_error_text(run, exc), + # ALWAYS set, and always an `int`. `duration_ms` is not guarded on + # `model_response`, and `durationOf` prefers the closing event's value + # over end-minus-start — which is what keeps model durations honest even + # though the execution graph pairs model events FIFO per agent_id. A + # float would silently NULL the promoted u32 column. + duration_ms=_duration_ms(run), + **extras, + ) + + +def _duration_ms(run: Any) -> int: + start = getattr(run, "start_time", None) + if start is None: + return 0 + # `_errored_llm_run` does not set `end_time`, unlike every other errored + # path, so an errored model call would report a 0ms duration without this. + end = getattr(run, "end_time", None) or _now() + return ms(end - start) + + +def _usage(response: Any) -> dict: + """Normalise token counts across the three shapes providers actually use. + + Primary is `usage_metadata` on the message — the LangChain-standard shape + since 0.3 and the only one that carries cache/reasoning detail. The two + fallbacks are OpenAI's `prompt_tokens`/`completion_tokens` and Anthropic's + `input_tokens`/`output_tokens`, both of which arrive under `llm_output`. + """ + if response is None: + return {} + message = _first_message(response) + data = getattr(message, "usage_metadata", None) + if isinstance(data, dict) and data: + usage = { + "input_tokens": data.get("input_tokens"), + "output_tokens": data.get("output_tokens"), + "total_tokens": data.get("total_tokens"), + } + for key in ("input_token_details", "output_token_details"): + if data.get(key): + usage[key] = dict(data[key]) + return {k: v for k, v in usage.items() if v is not None} + + output = getattr(response, "llm_output", None) + if not isinstance(output, dict): + return {} + raw = output.get("token_usage") or output.get("usage") or {} + if not isinstance(raw, dict): + return {} + # Built as an explicitly `int`-valued dict rather than filtered in place: + # the `isinstance` filter narrows at runtime but not for a type checker, and + # the arithmetic below is the kind of thing that must not be `Any`. + counts: dict[str, int] = { + name: value + for name, value in ( + ("input_tokens", raw.get("prompt_tokens", raw.get("input_tokens"))), + ("output_tokens", raw.get("completion_tokens", raw.get("output_tokens"))), + ("total_tokens", raw.get("total_tokens")), + ) + if isinstance(value, int) and not isinstance(value, bool) + } + if counts and "total_tokens" not in counts: + counts["total_tokens"] = counts.get("input_tokens", 0) + counts.get("output_tokens", 0) + return counts + + +def _first_generation(response: Any) -> Any: + generations = getattr(response, "generations", None) + if not generations: + return None + first = generations[0] + if isinstance(first, (list, tuple)): + return first[0] if first else None + return first + + +def _first_message(response: Any) -> Any: + generation = _first_generation(response) + return getattr(generation, "message", None) if generation is not None else None + + +def _completion(response: Any) -> tuple: + generation = _first_generation(response) + if generation is None: + return None, None, None + message = getattr(generation, "message", None) + content = getattr(message, "content", None) + if content is None: + content = getattr(generation, "text", None) + info = getattr(generation, "generation_info", None) or {} + stop = info.get("finish_reason") or info.get("stop_reason") + if not stop and message is not None: + response_meta = getattr(message, "response_metadata", None) or {} + stop = response_meta.get("finish_reason") or response_meta.get("stop_reason") + role = "assistant" if message is not None else None + return truncate(content, _FIELD_LIMIT), role, stop + + +def _end_root(run: Any, info: _RunInfo, meta: dict, exc: BaseException | None) -> None: + state = _STATE + session = info.session + state.runs.pop(info.id, None) + _close_open_leaves(info.id) + if session is None: + return + + if session.open_pauses: + # Interrupted, waiting on a human. Deliberately no `agent_end`: closing + # the agent here would force-close the open pause (the graph does that + # at `agent_end`), zeroing the one interval that measures how long the + # human took. The agent is closed by the resuming `.invoke()`. + return + + failed = exc is not None and not _is_control_flow(exc) + if not failed and getattr(run, "error", None) and not _is_control_flow(exc): + failed = True + + if failed and not session.reported_error: + # Nothing below reported this failure, so nobody owns it — a standalone + # `error` event is the only way it reaches the Errors surface. Strictly + # before `agent_end`: the graph closes the agent span at `agent_end`, + # so an error after it is attributed to nothing. + _emit_on_agent( + session, + "error", + error_type=type(exc).__name__ if exc is not None else "RunError", + message=_error_message(run, exc) or "run failed", + traceback=truncate(str(getattr(run, "error", "") or ""), _core.FIELD_LIMIT) or None, + **_fw_common(run, info, meta), + ) + + state.tracker.end_agent( + session.agent_key, + outcome="failed" if failed else "success", + summary=_error_text(run, exc) if failed else None, + **_fw_common(run, info, meta), + ) + state.sessions.pop(session.session_id, None) + + +# Mirrors `_ROOT_LEAF_STARTERS`. `_end_tool`/`_end_retriever` take no response +# argument, so they are adapted to one signature here rather than at the call +# site — a mismatch would be swallowed by `safe()` and read as "no events". +_ROOT_LEAF_ENDERS = { + "model": _end_model, + "tool": lambda run, info, meta, exc, response: _end_tool(run, info, meta, exc), + "retriever": lambda run, info, meta, exc, response: _end_retriever(run, info, meta, exc), +} + + +def _close_open_leaves(root_id: str) -> None: + """Close every leaf still open under this root. Caller holds the lock. + + `agent_end` force-closes open *pauses* but not tools, models or humans, so a + run that dies mid-tool leaves the session `ongoing` forever. Closing them + here is what keeps that invariant true when a framework skips an end + callback — which happens on a hard cancellation, a killed stream, or a + handler that was disabled part-way through by the failure policy. + """ + state = _STATE + stale = [i for i in state.runs.values() if i.root == root_id and i.id != root_id] + for info in reversed(stale): + state.runs.pop(info.id, None) + if info.hidden or not info.kind: + continue + marker = fw_fields(incomplete=True) + try: + if info.kind == "tool" or info.kind == "retriever": + _emit( + "tool_result", + info, + tool_name=info.name or "tool", + tool_call_id=info.tool_call_id or info.id, + **marker, + ) + elif info.kind in ("node", "chain"): + _emit( + "hook_completed", + info, + hook_name=info.node or info.name, + hook_id=info.id, + outcome="cancelled", + **marker, + ) + elif info.kind == "model": + _emit( + "model_response", + info, + request_id=info.id, + model=info.model, + stop_reason="incomplete", + duration_ms=ms(_now() - info.started), + **marker, + ) + elif info.kind == "subgraph": + state.tracker.end_agent(info.id, outcome="cancelled", **marker) + except Exception: # pragma: no cover - teardown must never raise + logger.debug("failproofai_sdk: could not close open span %s", info.id, exc_info=True) + + +# --------------------------------------------------------------------------- +# Human in the loop +# --------------------------------------------------------------------------- + +def _interrupts_of(exc: BaseException | None) -> tuple: + """The `Interrupt`s carried by a `GraphInterrupt`, if this is one. + + `GraphInterrupt.__init__` does `super().__init__(interrupts)`, so + `exc.args[0]` is the sequence. `ParentCommand` and `GraphDrained` are also + `GraphBubbleUp` but carry a `Command` / a reason string, so the duck-typed + `.value` check is what keeps them out. + """ + if not _is_control_flow(exc): + return () + args = getattr(exc, "args", ()) or () + if not args: + return () + candidates = args[0] + if not isinstance(candidates, (list, tuple)): + return () + return tuple(c for c in candidates if hasattr(c, "value")) + + +def _suspend(session: _Session, interrupts: Iterable) -> None: + """`human_wait` + `agent_pause`, one pair per `Interrupt`, in that order. + + **Both** pairs are required and neither is redundant: only + `agent_pause` -> `agent_resume` feeds the graph's `pausedMs` (without it the + session reports `ongoing` and inflates its active duration by the whole + human wait), and only `human_wait` -> `human_input` carries the prompt, the + response and the `pendingHuman` count. + """ + for index, interrupt in enumerate(interrupts): + pause_id = str(getattr(interrupt, "id", None) or "%s:%d" % (session.agent_key, index)) + if pause_id in session.open_pauses: + continue + prompt, options = _prompt_of(getattr(interrupt, "value", None)) + session.open_pauses[pause_id] = prompt + _emit_on_agent( + session, + "human_wait", + input_id=pause_id, + prompt=prompt, + options=options, + reason="langgraph_interrupt", + **fw_fields(interrupt_id=pause_id, kind="interrupt"), + ) + _emit_on_agent( + session, + "agent_pause", + pause_id=pause_id, + reason="langgraph_interrupt", + **fw_fields(interrupt_id=pause_id), + ) + + +def _prompt_of(value: Any) -> tuple: + if isinstance(value, dict): + prompt = value.get("prompt") or value.get("question") or value.get("message") + options = value.get("options") + if not isinstance(options, (list, tuple)): + options = None + else: + options = [str(o) for o in options] + if prompt is not None: + return truncate(str(prompt), _FIELD_LIMIT), options + return truncate(str(value), _FIELD_LIMIT), options + return truncate(str(value), _FIELD_LIMIT) if value is not None else None, None + + +def _resume(session: _Session, run: Any) -> None: + """`agent_resume` + `human_input`, in that order, one pair per open pause.""" + if not session.open_pauses: + return + answers = _resume_values(run) + for pause_id, prompt in list(session.open_pauses.items()): + session.open_pauses.pop(pause_id, None) + _emit_on_agent( + session, + "agent_resume", + pause_id=pause_id, + reason="langgraph_resume", + **fw_fields(interrupt_id=pause_id), + ) + _emit_on_agent( + session, + "human_input", + input_id=pause_id, + response=_answer_for(answers, pause_id), + **fw_fields(interrupt_id=pause_id, prompt=prompt), + ) + + +_MISSING = object() + + +def _steering_value(run: Any) -> Any: + """The object `.invoke()` was called with, when it was **not** fresh state. + + Verified on langgraph 1.2.11: a fresh turn arrives as the state mapping + itself (``{'trail': []}``), while anything that is not a mapping is wrapped + under a single ``input`` key — ``{'input': Command(resume='yes')}`` for a + resume, ``{'input': None}`` for ``invoke(None, config)``. So the presence of + that key is what separates "steering an existing checkpointed run" from + "starting a new one", and it is a *positive* test rather than a guess at + which state schemas happen to look like a Command. + """ + inputs = getattr(run, "inputs", None) + if isinstance(inputs, dict): + return inputs.get("input", _MISSING) + return inputs if inputs is not None else _MISSING + + +def _is_continuation(run: Any) -> bool: + """True when this root run continues an interrupted thread. + + LangGraph has exactly two of these — ``Command(...)`` and ``None`` — and + both are shaped unlike fresh state (see `_steering_value`). Everything else + starts a new run even when it lands on a thread that is mid-interrupt: a + fresh input discards the pending tasks rather than answering them. + """ + value = _steering_value(run) + if value is _MISSING: + return False + if value is None: + return True + if _Command is not None and isinstance(value, _Command): + return True + # Duck-typed fallback for a moved/renamed `Command`. + return all(hasattr(value, name) for name in ("resume", "goto", "update")) + + +def _resume_values(run: Any) -> Any: + """The value handed to `Command(resume=...)`, read off the root run's input. + + On a resume, langgraph calls `on_chain_start` with the `Command` itself as + the input, so the human's answer is available to us without any cooperation + from the caller. `Command(resume={interrupt_id: value})` (the multi- + interrupt form) is handled by `_answer_for`. + """ + value = _steering_value(run) + return getattr(value, "resume", None) if value is not _MISSING else None + + +def _answer_for(answers: Any, pause_id: str) -> str | None: + if answers is None: + return None + if isinstance(answers, dict) and pause_id in answers: + return truncate(str(answers[pause_id]), _FIELD_LIMIT) + return truncate(str(answers), _FIELD_LIMIT) + + +def _session_for_run(run_id: Any) -> _Session | None: + info = _STATE.runs.get(str(run_id)) if run_id is not None else None + return info.session if info is not None else None + + +# --------------------------------------------------------------------------- +# Human in the loop, resumed by a DIFFERENT PROCESS +# --------------------------------------------------------------------------- +# +# Everything above assumes the process that paused is the process that resumes, +# because it keys the pause on the `Interrupt` object it saw. Real HITL is not +# shaped like that: the interrupt is served by one worker, a human answers +# minutes or hours later, and any worker may pick that request up. The resuming +# process has no `_Session`, no `open_pauses`, and langgraph's `GraphResumeEvent` +# carries a checkpoint id but no interrupt ids — so there was nothing to +# correlate on and the pause simply stayed open forever. +# +# It is recoverable, exactly, because `Interrupt.id` is not random. langgraph +# 1.2's `interrupt()` builds it with `Interrupt.from_ns(value, ns)`, i.e. +# `xxh3_128(checkpoint_ns)` — a pure function of the interrupted task's +# namespace. That namespace is `metadata["langgraph_checkpoint_ns"]`, which this +# adapter already reads on every node run, and it is **byte-identical across the +# two invocations** (VERIFIED on langgraph 1.2.11: `approve:49c9e42f-…` in both +# the interrupting and the resuming process, hashing to the id the first process +# reported). So the resuming process can reconstruct the id the pausing process +# used without any shared state at all. +# +# The remaining question is *which* node re-ran because it was interrupted, and +# langgraph answers that too, in two parts: +# +# * `on_resume` fires once per Pregel level, in order, each naming the level's +# checkpoint namespace, and always **before** the node runs at that level. An +# interrupt inside a subgraph therefore produces `ns=()` then +# `ns=('child:…',)`, and the deepest of those is the graph that actually +# paused — which is how the subgraph HOST node (a normal node at the shallower +# level) is excluded. +# * only the level's first superstep re-runs interrupted tasks. A sibling that +# had already succeeded in that superstep does not re-run at all, and +# downstream nodes are at later steps. +# +# Deliberately decided at node **end** rather than start: a subgraph host node +# starts before the deeper `on_resume` that unmasks it, so at start time it is +# indistinguishable from the interrupted task. The cost is that `agent_resume` +# lands after the resumed node's own body, which adds that node's duration to +# the measured wait — a rounding error against a human, and the only alternative +# is guessing. + + +def _remote_of(info: _RunInfo) -> _RemoteResume | None: + """The `_RemoteResume` of this run's root, if the root is one.""" + root = _STATE.runs.get(info.root) if info.root else None + return root.remote if root is not None else None + + +def _interrupt_id_of(ns: str) -> str | None: + if _Interrupt is None or not ns: + return None + try: + return str(_Interrupt.from_ns(None, ns).id) + except Exception: # pragma: no cover - a future langgraph changing the shape + logger.debug("failproofai_sdk: could not derive an interrupt id", exc_info=True) + return None + + +def _close_remote_pause(info: _RunInfo, meta: dict) -> None: + """`agent_resume` + `human_input` for a pause this process never opened.""" + remote = _remote_of(info) + session = info.session + if remote is None or session is None or remote.deepest is None: + return + parts = _ns_parts(meta) + level = tuple(parts[:-1]) + if level != remote.deepest: + return + if meta.get("langgraph_step") != remote.levels.get(level): + return + pause_id = _interrupt_id_of(meta.get("langgraph_checkpoint_ns") or "") + if pause_id is None or pause_id in remote.done: + return + remote.done.add(pause_id) + marker = fw_fields(interrupt_id=pause_id, resumed_elsewhere=True) + _emit_on_agent( + session, "agent_resume", pause_id=pause_id, reason="langgraph_resume", **marker + ) + _emit_on_agent( + session, + "human_input", + input_id=pause_id, + response=_answer_for(remote.value, pause_id), + **marker, + ) + + +# --------------------------------------------------------------------------- +# The handler +# --------------------------------------------------------------------------- + +class FailproofAITracer(BaseTracer, _GraphBase): + """The single sync handler. Zero-arg, cheap, and stateless by design. + + Every override is one of two shapes: + + * ``_start_trace`` / ``_end_trace`` — call `super()` and hand the assembled + `Run` to a module-level translator wrapped in `_core.safe`. `super()` is + called **unconditionally and outside** our own work, so a bug in the + translator can never skip LangChain's own bookkeeping. + * ``on_*`` — stash the one thing the `Run` object does not preserve (the + exception object, the `LLMResult`, the un-flattened chat messages), then + delegate. These exist because `Run.error` is a formatted traceback rather + than the exception, and we need `isinstance(exc, GraphBubbleUp)` to tell a + human-approval pause from a failure. + + Nothing here holds a contextvar token: `ContextVar.reset()` raises across + tasks as well as threads, and every one of these callbacks can land on a + different task from the one that opened the run. + """ + + # Non-negotiable. See the module docstring: without it AsyncCallbackManager + # dispatches us through run_in_executor and can reorder our callbacks. + run_inline = True + + @property + def raise_error(self) -> bool: # type: ignore[override] + """False normally; True under FAILPROOFAI_SDK_STRICT. + + Normally False so an adapter bug can never take down the customer's + graph: LangChain catches, logs and swallows handler exceptions, and + `_core.safe` does the same one layer further in. + + But that firewall also made `FAILPROOFAI_SDK_STRICT=1` inert *specifically + here*. `safe()` re-raises under strict, and LangChain's `handle_event` + then caught it and logged "Error in FailproofAITracer. callback", so + the fault never reached the caller and the escape hatch silently did + nothing on the one adapter people are most likely to debug. Following + strict mode restores it. Read per callback by LangChain, so toggling + the env var takes effect without re-instrumenting. + """ + return _core.strict() + + def _persist_run(self, run: Any) -> None: + """Required by `BaseTracer`; we stream, so there is nothing to persist.""" + + def _start_trace(self, run: Any) -> None: + super()._start_trace(run) + _on_start(run) + + def _end_trace(self, run: Any) -> None: + _on_end(run) + super()._end_trace(run) + + def on_chat_model_start( + self, + serialized: dict, + messages: list, + *, + run_id: Any, + tags: list | None = None, + parent_run_id: Any = None, + metadata: dict | None = None, + name: str | None = None, + **kwargs: Any, + ) -> Any: + _stash_messages(run_id, messages) + return super().on_chat_model_start( + serialized, + messages, + run_id=run_id, + tags=tags, + parent_run_id=parent_run_id, + metadata=metadata, + name=name, + **kwargs, + ) + + def on_llm_end(self, response: Any, *, run_id: Any, **kwargs: Any) -> Any: + _stash(run_id, response) + return super().on_llm_end(response, run_id=run_id, **kwargs) + + def on_llm_error(self, error: BaseException, *, run_id: Any, **kwargs: Any) -> Any: + _stash_error(run_id, error) + return super().on_llm_error(error, run_id=run_id, **kwargs) + + def on_chain_error( + self, error: BaseException, *, inputs: dict | None = None, run_id: Any, **kwargs: Any + ) -> Any: + _stash_error(run_id, error) + return super().on_chain_error(error, inputs=inputs, run_id=run_id, **kwargs) + + def on_tool_error(self, error: BaseException, *, run_id: Any, **kwargs: Any) -> Any: + _stash_error(run_id, error) + return super().on_tool_error(error, run_id=run_id, **kwargs) + + def on_retriever_error(self, error: BaseException, *, run_id: Any, **kwargs: Any) -> Any: + _stash_error(run_id, error) + return super().on_retriever_error(error, run_id=run_id, **kwargs) + + def _on_llm_new_token(self, run: Any, token: Any, chunk: Any) -> None: + """Folded into the closing `model_response`. **Never** an event. + + A 500-token response would otherwise be 500 stored rows and 500 rail + rows against a five-lane cap. Langfuse uses this callback only to stamp + time-to-first-token; so do we. + """ + _count_token(run) + + def on_interrupt(self, event: Any) -> None: + _on_interrupt(event) + + def on_resume(self, event: Any) -> None: + _on_resume(event) + + +# The translators, each individually guarded. `safe()` catches `Exception` and +# **not** `BaseException`: `CancelledError`, `KeyboardInterrupt` and +# `SystemExit` are BaseExceptions, and swallowing them here would silently break +# cancellation in every instrumented async application. +_on_start = safe(_on_start) +_on_end = safe(_on_end) + + +@safe +def _stash(run_id: Any, response: Any) -> None: + with _STATE.lock: + _STATE.responses[str(run_id)] = response + + +@safe +def _stash_messages(run_id: Any, messages: Any) -> None: + # The one stash `_on_end` does not clean up after itself: it pops `rid` and + # `messages:` is a different key, drained only by `_start_model`. So it is + # the one that has to honour the kill switch too, or a torn-down adapter + # grows a dict forever. + if not _STATE.enabled: + return + with _STATE.lock: + _STATE.responses["messages:" + str(run_id)] = _normalize_messages(messages) + + +@safe +def _stash_error(run_id: Any, error: BaseException) -> None: + with _STATE.lock: + _STATE.errors[str(run_id)] = error + + +@safe +def _count_token(run: Any) -> None: + with _STATE.lock: + info = _STATE.runs.get(str(run.id)) + if info is None: + return + info.chunks += 1 + if info.ttft_ms is None: + start = getattr(run, "start_time", None) + info.ttft_ms = ms(_now() - start) if start is not None else 0 + + +@safe +def _on_interrupt(event: Any) -> None: + with _STATE.lock: + session = _session_for_run(getattr(event, "run_id", None)) + if session is None: + return + _suspend(session, getattr(event, "interrupts", ()) or ()) + + +@safe +def _on_resume(event: Any) -> None: + # Two jobs. The first is normally a no-op: the resuming root run starts + # *before* langgraph drains its lifecycle queue, so `_start_root` has + # already closed a pause this process opened. That is here for the ordering + # not holding in some future version, and `_resume` returns immediately when + # there is nothing open. + # + # The second is load-bearing, and is the only signal that separates the + # subgraph HOST node from the task that actually paused: this event names + # the Pregel level that is resuming, and fires once per level, deepest last. + with _STATE.lock: + info = _STATE.runs.get(str(getattr(event, "run_id", None) or "")) + if info is None: + return + # `_remote_of` resolves through `info.root`, which a root run sets to + # its own id, so this covers both the root's event and a subgraph's. + remote = _remote_of(info) + if remote is not None: + level = tuple(getattr(event, "checkpoint_ns", ()) or ()) + if remote.deepest is None or len(level) >= len(remote.deepest): + remote.deepest = level + if info.session is not None: + _resume(info.session, None) + + +# --------------------------------------------------------------------------- +# Install / uninstall +# --------------------------------------------------------------------------- + +_HANDLER_VAR: contextvars.ContextVar = contextvars.ContextVar( + "failproofai_langchain_handler", default=None +) + +_hook_lock = threading.Lock() +_hook_registered = False + +# The instance `install()` created, kept outside the ContextVar so that a worker +# thread — which starts with a fresh context and therefore an empty var — can +# still find it. `_configure` itself does not need this (it constructs a fresh +# zero-arg handler from the env var), but the graph-lifecycle wrap does, because +# langgraph filters on `isinstance`, not on a class. +_ACTIVE_HANDLER: Any = None + + +def _register_hook() -> None: + """`register_configure_hook` exactly once per process. + + `_configure_hooks` is a module-level list with no removal API, so calling + this twice means two entries — and although `_configure`'s `isinstance` + dedup would keep the handler count at one, the list would grow on every + `instrument()`/`uninstrument()` cycle in a reloading dev server. + """ + global _hook_registered + with _hook_lock: + if _hook_registered: + return + register_configure_hook(_HANDLER_VAR, True, FailproofAITracer, ENV_VAR) + _hook_registered = True + + +@safe +def _attach_graph_handler(manager: Any) -> None: + handler = _HANDLER_VAR.get() or _ACTIVE_HANDLER + if handler is None or manager is None: + return + handlers = getattr(manager, "handlers", None) + if handlers is None or any(isinstance(h, FailproofAITracer) for h in handlers): + return + manager.add_handler(handler, True) + + +def _install_graph_callbacks(patcher: Patcher) -> bool: + """Make `on_interrupt`/`on_resume` reach a globally-installed handler. + + Verified on langgraph 1.2.10: `Pregel.stream` calls + `get_sync_graph_callback_manager_for_config(config)`, which filters the + **raw** `config["callbacks"]` for `GraphCallbackHandler` instances. A + handler injected by `register_configure_hook` is never in there — the hook + runs inside `CallbackManager.configure`, which builds a *different* + manager — so without this wrap the lifecycle callbacks are dead code for + every user who did not pass the handler by hand. Worse, langgraph gates the + feature entirely on `has_graph_lifecycle_callbacks=bool(manager.handlers)`. + + We patch the names as they are bound in `langgraph.pregel.main` (a + `from ... import`, so patching `langgraph.callbacks` would have no effect) + and only when they are still the same objects, so a refactor upstream + degrades to "no lifecycle callbacks" rather than to a wrong patch. + """ + import langgraph.callbacks as lgcb + import langgraph.pregel.main as pmain + + names = ( + "get_sync_graph_callback_manager_for_config", + "get_async_graph_callback_manager_for_config", + ) + for name in names: + bound = getattr(pmain, name, None) + if bound is None or bound is not getattr(lgcb, name, None): + return False + for name in names: + original = getattr(pmain, name) + patcher.patch( + pmain, + name, + # `wrap_callable` is the structural guarantee: the original call is + # the only thing inside the try, and `_attach_graph_handler` runs + # outside it and inside `call_safely`. The manager is mutated in + # place, so nothing about the returned object changes. + _core.wrap_callable(original, after=lambda _ctx, manager: _attach_graph_handler(manager)), + ) + return True + + +class _Adapter: + """The object `failproofai_sdk.integrations` looks for as `adapter`.""" + + name = NAME + module = MODULE + + def __init__(self) -> None: + self._patcher = Patcher() + self._handler: FailproofAITracer | None = None + self._set_env = False + + def install(self, **options: Any) -> None: + _compat.check_version( + NAME, + DIST, + minimum="1.4.7", + below="2", + reason="langgraph 1.2's own floor; earlier cores lack the metadata this adapter reads", + ) + _compat.check_version(NAME, "langgraph", minimum="1.2", below="2", reason="GraphCallbackHandler") + + _STATE.options = _read_options(options) + _STATE.reset() + _STATE.tracker = RunTracker(NAME, base_fields=_base_fields()) + _STATE.enabled = True + + global _ACTIVE_HANDLER + _register_hook() + self._handler = FailproofAITracer() + _ACTIVE_HANDLER = self._handler + _HANDLER_VAR.set(self._handler) + # The ContextVar only reaches contexts derived from this one, so a + # worker thread started later would not see it. The env var is what + # covers those: `_configure` constructs a fresh zero-arg handler when + # the var is empty, which is safe precisely because all state is in + # `_STATE` rather than on the instance. + if ENV_VAR not in os.environ: + os.environ[ENV_VAR] = "1" + self._set_env = True + + if _STATE.options.graph_callbacks and _GraphCallbackHandler is not None: + if _compat.probe(NAME, "graph_lifecycle_callbacks", lambda: _install_graph_callbacks(self._patcher)): + logger.debug("failproofai_sdk: langgraph interrupt/resume callbacks wired") + + def uninstall(self) -> None: + # There is no deregister API for a configure hook — `_configure_hooks` + # is append-only and private — so removal is "make the hook produce + # nothing": flip the kill switch, clear the ContextVar, unset the env + # var. The switch goes FIRST and is the only one of the three that + # cannot be routed around (see `_State.enabled`); it is flipped before + # `_close_everything()` because that path emits through the tracker + # directly and never re-enters `_on_start`. + global _ACTIVE_HANDLER + _STATE.enabled = False + _ACTIVE_HANDLER = None + _HANDLER_VAR.set(None) + if self._set_env: + os.environ.pop(ENV_VAR, None) + self._set_env = False + self._patcher.restore_all() + self._handler = None + _close_everything() + _STATE.reset() + _STATE.options = _Options() + + +def _read_options(options: dict) -> _Options: + include = options.get("include_chains") or () + if isinstance(include, str): + include = (include,) + unknown = set(options) - {"session_id", "include_chains", "capture_content", "graph_callbacks"} + if unknown: + # Not fatal: `instrument()` with no name installs every detected + # adapter with the same **options, so an option meant for CrewAI + # legitimately arrives here. + logger.debug("failproofai_sdk: langchain adapter ignoring options %s", sorted(unknown)) + return _Options( + session_id=options.get("session_id"), + include_chains=frozenset(str(name) for name in include), + capture_content=bool(options.get("capture_content", True)), + graph_callbacks=bool(options.get("graph_callbacks", True)), + ) + + +def _close_everything() -> None: + """Close every span still open at teardown, leaves before agents.""" + with _STATE.lock: + roots = {info.root for info in _STATE.runs.values() if info.root} + for root in roots: + _close_open_leaves(root) + _STATE.tracker.close_open_agents(outcome="cancelled") + + +adapter = _Adapter() diff --git a/sdk/python/failproofai_sdk/integrations/llama_index.py b/sdk/python/failproofai_sdk/integrations/llama_index.py new file mode 100644 index 000000000..ef6bf5ecb --- /dev/null +++ b/sdk/python/failproofai_sdk/integrations/llama_index.py @@ -0,0 +1,1524 @@ +"""LlamaIndex adapter — written against llama-index-core 0.14.23 (2026-07-29). + + import failproofai_sdk + from llama_index.core.agent.workflow import FunctionAgent + + failproofai_sdk.instrument("llama_index") + await FunctionAgent(name="researcher", tools=[...], llm=llm).run("...") + +Everything is registered on the **root dispatcher** (`get_dispatcher()` with no +argument). Child dispatchers propagate upward, so one handler pair on the root +sees every span and every event in the process — no per-object wiring, no call +site changes. + +What most tutorials (and several shipping vendors) get wrong here +----------------------------------------------------------------- +Each of these was verified against the installed package, not recalled: + +1. ``llama_index.core.instrumentation`` is a **shim** over the separately + released ``llama-index-instrumentation`` distribution. Import through the + ``llama_index.core`` path anyway — it is the stable name. + +2. **The classic agent events are dead.** ``AgentRunStepStartEvent``, + ``AgentChatWithStepStartEvent`` and ``AgentToolCallEvent`` still *import*, + but nothing has emitted them since the 0.13.0 agent rewrite, so an adapter + built on them records nothing and raises nothing. Agent structure now lives + in the **workflow** stream, and we reach the typed objects through the span + handler: a workflow step span carries its input event in + ``bound_args.arguments["ev"]`` and hands back its output event as the span + ``result``. That is where ``AgentInput`` / ``AgentSetup`` / ``AgentOutput`` / + ``ToolCall`` / ``ToolCallResult`` actually are. + +3. **The dispatcher swallows handler exceptions** with a bare + ``except BaseException: pass`` **and no logging** (verified in + ``llama_index_instrumentation.dispatcher``). A bug in a handler is therefore + completely invisible. Every entry point below is wrapped in ``_core.safe``, + whose whole job is to log the thing the dispatcher would have eaten. + +4. **Never read ``event.model_dict["model"]``.** PR #22130 (shipped in 0.14.23) + replaced ``to_dict()`` with ``to_payload()`` and the ``"model"`` key is gone; + reading it yields ``None`` silently. Traceloop and MLflow are broken on this + today. We read ``instance.metadata.model_name`` off the LLM span instead, and + only fall back to ``model_dict.get("model_name")``. + +5. ``new_span`` takes **``parent_span_id``** while ``span_enter`` takes + ``parent_id``. Getting that wrong gives a flat trace with no error. + +6. Teardown needs **in-place slice assignment** — ``add_span_handler`` does + ``self.span_handlers += [h]``, so a plain ``=`` rebinds a pydantic field and + other handlers can be lost. + +Mapping +------- +============================ ========================================== +LlamaIndex Failproof AI +============================ ========================================== +``Workflow.run`` root span session + ``agent_start``/``agent_end`` +nested ``Workflow.run`` span nested ``agent_start``/``agent_end`` +``AgentWorkflow`` handoff nested ``agent_start``/``agent_end`` per + ``current_agent_name`` (see ``_sub_agent``) +workflow step span ``hook_triggered``/``hook_completed`` + (``trigger_event="workflow_step"``) +``SpanCancelledEvent`` ``outcome="cancelled"`` on the run or the step +``LLMChatStart/EndEvent`` ``model_request``/``model_response`` + (``request_id=event.span_id``) +``FunctionTool.call`` span ``tool_use``/``tool_result`` +``RetrievalStart/EndEvent`` ``tool_use``/``tool_result``, output summarized +embeddings nothing, unless ``embeddings=True`` +``WaitingForEvent`` drop ``human_wait``+``agent_pause``, then + ``agent_resume``+``human_input`` on retry +============================ ========================================== + +``agent_id`` is the ``FunctionAgent.name`` when there is one and the workflow +class name otherwise — never a span id. It is a ``LowCardinality`` column and +the primary dashboard facet; a uuid in it poisons that facet permanently. + +``AgentWorkflow`` needs one more step to keep that promise. It does **not** run +its agents as nested workflows — there is a single ``AgentWorkflow.run`` span +and the agents are steps inside it — so read off the span tree alone a two-agent +crew lands as one ``agent_id="AgentWorkflow"`` and the handoff is invisible. The +runtime does say who holds the turn, on every ``AgentInput``/``AgentSetup``/ +``AgentOutput`` a step is invoked with: ``current_agent_name``. Each distinct +name therefore opens a nested agent under the workflow and a handoff closes the +previous one, which is what puts ``researcher`` and ``analyst`` in the facet +rather than in a payload extra nobody can group by. + +Token fidelity is genuinely lower on LlamaIndex than on the other frameworks +----------------------------------------------------------------------------- +There is no standard usage field. We try ``response.raw["usage"]``, then +``raw["usage_metadata"]``, then ``response.additional_kwargs``, calling +``model_dump()`` first when ``raw`` is a pydantic model. The top-level +``input_tokens``/``output_tokens`` are set **only** when a key we recognise is +present; the raw dict always ships as ``usage`` so the server and the dashboard +can both fall back to it. A model integration that names its counters something +new will show a populated ``usage`` and blank token columns — that is the +honest outcome, and much better than a confident wrong number. + +**Streaming has no usage at all, and that is the default path.** +``FunctionAgent`` — the agent api LlamaIndex documents — calls +``astream_chat``, and llama-index-llms-openai does not send +``stream_options={"include_usage": True}``, so the provider never emits the +usage chunk and ``LLMChatEndEvent.response.raw`` has no ``usage`` key to find. +Verified against llama-index-core 0.14.23 by spying on the dispatcher directly: +every ``LLMChatEndEvent`` in a ``FunctionAgent`` run arrives with usage absent. +Nothing in this adapter can recover a number the framework never received. The +user-side fix is one argument, and it works:: + + OpenAI(model=..., additional_kwargs={"stream_options": {"include_usage": True}}) + +Non-streaming calls (``llm.chat`` / ``llm.achat``) extract usage correctly with +no extra configuration. + +Known gap: human-in-the-loop is only visible when the wait happens **inside a +tool**. ``ctx.wait_for_event`` in a plain workflow step is caught by the runtime +before it reaches the dispatcher, so that step simply exits with ``None`` and +re-runs later; there is no signal to key a pause on. The FunctionAgent pattern +(the one LlamaIndex documents) waits inside a tool and is captured. +""" + +from __future__ import annotations + +import re +import threading +import time +import uuid +from dataclasses import dataclass, field +from typing import Any, Callable + +from failproofai_sdk.integrations import _compat, _core + +FRAMEWORK = "llama_index" +DIST = "llama-index-core" +EXTRA = "llamaindex" + +# 0.14.23 is a CAPABILITY floor, not a guess: it is the release where +# `to_payload()` replaced `to_dict()` (PR #22130) and where the workflow event +# stream carries the typed agent events this adapter reads. Below it, model +# names and agent structure both go missing. +MIN_VERSION = "0.14.23" +BELOW_VERSION = "0.15" + +# "{ClassName}.{method}-{uuid4}" — the dispatcher's span id format. +_SPAN_ID = re.compile( + r"^(?P[^.]+)\.(?P.+)" + r"-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" +) + +# LLM methods worth an event. `_`-prefixed methods (`_prepare_chat_with_tools`) +# are framework plumbing and are ignored everywhere. +_LLM_METHODS = frozenset( + { + "chat", + "achat", + "stream_chat", + "astream_chat", + "complete", + "acomplete", + "stream_complete", + "astream_complete", + "predict", + "apredict", + } +) + +_SUMMARY_LIMIT = 512 +_MAX_SPANS = 20_000 +_MAX_NODES_IN_SUMMARY = 5 + +# Token key aliases, widest first. LlamaIndex normalises nothing, so this is the +# union of what the popular model integrations actually put in `raw`. +_INPUT_TOKEN_KEYS = ( + "prompt_tokens", + "input_tokens", + "inputTokens", + "prompt_token_count", + "promptTokenCount", +) +_OUTPUT_TOKEN_KEYS = ( + "completion_tokens", + "output_tokens", + "outputTokens", + "candidates_token_count", + "candidatesTokenCount", +) + + +# --------------------------------------------------------------------------- +# Helpers — no framework import in any of these +# --------------------------------------------------------------------------- + +def _span_parts(span_id: str) -> tuple[str, str]: + """`("FunctionAgent", "run")` from a dispatcher span id. Never raises.""" + match = _SPAN_ID.match(span_id or "") + if match is None: + text = str(span_id or "") + head = text.rsplit("-", 5)[0] if "-" in text else text + cls, _, method = head.partition(".") + return cls, method or head + return match.group("cls"), match.group("method") + + +def _summarize(value: Any, limit: int = _SUMMARY_LIMIT) -> str | None: + """A short, human-readable rendering. `None` in, `None` out.""" + if value is None: + return None + try: + text = str(value) + except Exception: + text = repr(value) + if not text: + # Several workflow events override `__str__` to return the response + # text, which is empty on a pure tool-call turn — and a free-form + # `Event(prefix="ok?")` keeps its payload in `_data`, so both `str()` + # and `repr()` render it as `InputRequiredEvent()`. An empty string in + # the dashboard reads as "we captured nothing". + try: + data = getattr(value, "_data", None) + if isinstance(data, dict) and data: + text = f"{type(value).__name__}({data})" + else: + text = repr(value) + except Exception: + text = type(value).__name__ + return _core.truncate(text, limit) + + +def _as_dict(value: Any) -> dict | None: + """A dict view of a pydantic model, a dict, or nothing.""" + if isinstance(value, dict): + return value + dump = getattr(value, "model_dump", None) + if callable(dump): + try: + dumped = dump() + except Exception: + return None + return dumped if isinstance(dumped, dict) else None + return None + + +def _first_int(source: dict, keys: tuple[str, ...]) -> int | None: + for key in keys: + value = source.get(key) + if isinstance(value, bool): + continue + if isinstance(value, int): + return value + if isinstance(value, float) and value.is_integer(): + return int(value) + # Bedrock hands back list-valued counts on some models. + if isinstance(value, (list, tuple)) and len(value) == 1: + inner = value[0] + if isinstance(inner, int) and not isinstance(inner, bool): + return inner + return None + + +def extract_usage(response: Any) -> tuple[dict | None, int | None, int | None]: + """`(usage_dict, input_tokens, output_tokens)` from a ChatResponse. + + Deliberately conservative: the token ints are returned **only** when a key + we recognise is present. Everything found ships as `usage` regardless, so a + model whose counters we cannot name still reports something the server's + summary and the dashboard can fall back to. + """ + if response is None: + return None, None, None + raw = _as_dict(getattr(response, "raw", None)) or {} + additional = getattr(response, "additional_kwargs", None) + additional = additional if isinstance(additional, dict) else {} + + usage: dict | None = None + for candidate in (raw.get("usage"), raw.get("usage_metadata"), additional.get("usage")): + as_dict = _as_dict(candidate) + if as_dict: + usage = as_dict + break + if usage is None and any(k in additional for k in _INPUT_TOKEN_KEYS + _OUTPUT_TOKEN_KEYS): + usage = { + k: v for k, v in additional.items() if k in _INPUT_TOKEN_KEYS + _OUTPUT_TOKEN_KEYS + } + if not usage: + return None, None, None + return usage, _first_int(usage, _INPUT_TOKEN_KEYS), _first_int(usage, _OUTPUT_TOKEN_KEYS) + + +def _messages(items: Any) -> list[dict] | None: + """ChatMessages -> the list-of-dicts `model_request(messages=...)` wants.""" + if not isinstance(items, (list, tuple)): + return None + out: list[dict] = [] + for item in items: + role = getattr(item, "role", None) + out.append( + { + "role": getattr(role, "value", None) or str(role or "user"), + "content": _summarize(getattr(item, "content", None) or ""), + } + ) + return out or None + + +def summarize_nodes(nodes: Any) -> dict: + """A retrieval result small enough to store. + + Retrieved documents are the largest strings in the process and the payload + is not a promoted column, so querying it means `JSONExtract` over the whole + blob. We keep the count, the scores and a prefix of the top few. + """ + items = list(nodes) if isinstance(nodes, (list, tuple)) else [] + top = [] + for node in items[:_MAX_NODES_IN_SUMMARY]: + inner = getattr(node, "node", node) + text = None + getter = getattr(inner, "get_content", None) + if callable(getter): + try: + text = getter() + except Exception: + text = None + if text is None: + text = getattr(inner, "text", None) + top.append( + { + "id": _summarize(getattr(inner, "node_id", None), 128), + "score": getattr(node, "score", None), + "text": _core.truncate(str(text or ""), 200), + } + ) + return {"num_nodes": len(items), "top": top} + + +def _error_text(exc: BaseException) -> str: + return f"{type(exc).__name__}: {exc}" + + +def _is_waiting(exc: BaseException | None) -> bool: + """True for the runtime's `WaitingForEvent` — a PAUSE, never an error. + + Name-based on purpose. `WaitingForEvent` lives in + `workflows.runtime.types.results`, is not re-exported from + `workflows.errors`, and has moved before; an `isinstance` against an import + that quietly failed would go always-False and turn every human-in-the-loop + pause into a red error event, which is exactly the failure this adapter + exists to avoid. + """ + if exc is None: + return False + return any(cls.__name__ == "WaitingForEvent" for cls in type(exc).__mro__) + + +def _is_cancellation(exc: BaseException | None) -> bool: + if exc is None: + return False + names = {cls.__name__ for cls in type(exc).__mro__} + return bool(names & {"CancelledError", "GeneratorExit"}) + + +# --------------------------------------------------------------------------- +# State +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class _Span: + span_id: str + parent_id: str | None + run_id: str | None # the enclosing agent span, or None if we never saw one + kind: str # agent | step | tool | model | retrieval | embedding | other + cls: str # the class half of the span id + name: str # the method half + started: float + + +@dataclass +class _Leaf: + """An emitted opener with no closer yet. Invariant 4 lives here.""" + + kind: str # tool | model | retrieval | embedding + span_id: str + parent_id: str | None + name: str + call_id: str + started: float + model: str | None = None + + +@dataclass +class _Run: + span_id: str + agent_id: str + open_leaves: dict[str, _Leaf] = field(default_factory=dict) + pauses: dict[str, str] = field(default_factory=dict) # waiter_id -> pause_id + used_tool_ids: dict[str, int] = field(default_factory=dict) + errors: int = 0 + # The `AgentWorkflow` sub-agent currently holding the turn, if any. See + # `_sub_agent`. Only ever set on a workflow run's own `_Run`; a sub-agent's + # `_Run` never opens a sub-agent of its own. + sub_key: str | None = None + sub_name: str | None = None + sub_seq: int = 0 + + +class _State: + """Everything the two handlers share. One lock, no contextvars. + + A span's start and its end are separate dispatcher calls that may land on + different tasks, so `ContextVar.reset(token)` is unusable here (it raises + across tasks as well as threads). Identity therefore comes from + `_core.RunTracker`, which passes `session_id=`/`agent_id=` explicitly. + """ + + def __init__(self, **options: Any) -> None: + self.embeddings = bool(options.get("embeddings", False)) + self.steps = bool(options.get("steps", True)) + self.capture_messages = bool(options.get("capture_messages", True)) + self.stale_after = float(options.get("stale_after", 600.0)) + self.reaper_interval = float(options.get("reaper_interval", 30.0)) + + self.tracker = _core.RunTracker( + FRAMEWORK, + base_fields=_core.framework_fields(FRAMEWORK, DIST), + ) + self._lock = threading.RLock() + self._spans: dict[str, _Span] = {} + self._runs: dict[str, _Run] = {} + # Per-instance, NOT class attributes: two _State objects (an + # install/uninstall cycle, or a test) must not share a span table. + self._step_inputs: dict[str, Any] = {} + self._model_names: dict[str, str] = {} + # span_id -> the run that owns its open leaf. A parked streaming span + # has already EXITED (and been forgotten from `_spans`) by the time its + # LLMChatEndEvent arrives, so the leaf cannot be found by walking the + # span tree — without this, every streaming model_response would be + # deferred to teardown and report the whole run as its duration. + self._leaf_run: dict[str, str] = {} + # Span ids the runtime told us were CANCELLED. `SpanCancelledEvent` is + # dispatched immediately before the matching `span_exit`, which the + # runtime deliberately performs with `result=None` and no error ("exit + # the span cleanly so it shows as OK rather than ERROR in traces" — + # workflows/runtime/types/step_function.py). Without this mark a + # user-pressed stop button is indistinguishable from a completed run, + # and every cancellation is reported as a success. + self._cancelled: set[str] = set() + self._stop = threading.Event() + self._reaper: threading.Thread | None = None + + # -- bookkeeping ------------------------------------------------------ + + def _remember(self, span: _Span) -> None: + while len(self._spans) >= _MAX_SPANS: + self._spans.pop(next(iter(self._spans)), None) + self._spans[span.span_id] = span + + def _run_of(self, span_id: str | None) -> _Run | None: + if span_id is None: + return None + span = self._spans.get(span_id) + if span is None or span.run_id is None: + return None + return self._runs.get(span.run_id) + + def _tool_call_id(self, run: _Run | None, raw_id: str | None, span_id: str) -> str: + """The framework's own tool id, kept verbatim where it is unambiguous. + + Passing the framework id through unchanged is what makes our events line + up with the customer's provider logs. But a human-in-the-loop tool is + re-run with the *same* `tool_id` after the human answers, and two pairs + sharing a `tool_call_id` in one session would pair wrongly, so a repeat + gets a `#n` suffix rather than a collision. + """ + if not raw_id: + return span_id + if run is None: + return raw_id + seen = run.used_tool_ids.get(raw_id, 0) + run.used_tool_ids[raw_id] = seen + 1 + return raw_id if seen == 0 else f"{raw_id}#{seen}" + + # -- spans ------------------------------------------------------------ + + def span_enter( + self, + span_id: str, + bound_args: Any, + instance: Any, + parent_span_id: str | None, + tags: dict | None, + ) -> None: + cls_name, method = _span_parts(span_id) + arguments = getattr(bound_args, "arguments", None) or {} + kind = self._classify(method, instance, parent_span_id) + + with self._lock: + parent = self._spans.get(parent_span_id) if parent_span_id else None + run_id = span_id if kind == "agent" else (parent.run_id if parent else None) + # An `AgentWorkflow` step belongs to the sub-agent holding the turn, + # not to the workflow. Resolving that BEFORE the span is remembered + # is what makes everything underneath it — the step's own hook pair, + # its tool calls, its LLM calls — resolve to the sub-agent too, + # because they all reach identity through this span's parent chain. + parent_key = parent_span_id + if kind == "step": + sub = self._sub_agent(parent, arguments) + if sub is not None: + parent_key = sub + run_id = sub + # Link every span, including the ones we emit nothing for: a chain + # of "other" spans between a leaf and its agent must not break + # identity resolution. + self.tracker.link(span_id, parent_key) + self._remember( + _Span( + span_id=span_id, + parent_id=parent_key, + run_id=run_id, + kind=kind, + cls=cls_name, + name=method, + started=time.monotonic(), + ) + ) + if kind == "agent": + self._start_agent(span_id, parent_span_id, instance, cls_name, arguments) + elif kind == "step": + self._start_step(span_id, parent_key, method, arguments) + elif kind == "tool": + self._start_tool(span_id, parent_span_id, instance, arguments) + elif kind == "model": + # The model leaf opens on LLMChatStartEvent, not here — the + # event is what carries the messages. All we need from the span + # is the model NAME, which the event no longer has (see the + # `to_payload()` note at the top). + pass + + def span_exit(self, span_id: str, bound_args: Any, instance: Any, result: Any) -> None: + with self._lock: + span = self._spans.pop(span_id, None) + cancelled = span_id in self._cancelled + self._cancelled.discard(span_id) + if span is None: + return + if span.kind == "agent": + self._end_agent( + span, + result=result, + outcome="cancelled" if cancelled else "success", + ) + elif span.kind == "step": + self._end_step(span, result=result, cancelled=cancelled) + elif span.kind == "tool": + self._close_leaf(span, output=result) + # A streaming LLM span exits the moment the generator is created, + # long before the stream is consumed. Its model leaf stays parked + # until LLMChatEndEvent, teardown, or the reaper. + + def span_drop(self, span_id: str, bound_args: Any, instance: Any, err: BaseException | None) -> None: + with self._lock: + span = self._spans.pop(span_id, None) + self._cancelled.discard(span_id) + if span is None: + return + if _is_waiting(err): + self._pause(span, err) + return + cancelled = _is_cancellation(err) + if span.kind == "agent": + self._end_agent( + span, + result=None, + outcome="cancelled" if cancelled else "failed", + error=None if cancelled else err, + ) + elif span.kind == "step": + self._end_step(span, result=None, error=None if cancelled else err) + else: + self._close_leaf(span, output=None, error=None if cancelled else err) + + def _classify(self, method: str, instance: Any, parent_span_id: str | None) -> str: + if method.startswith("_"): + return "other" + bases = _bases() + workflow = bases.get("Workflow") + if workflow is not None and isinstance(instance, workflow) and method == "run": + # Root or nested: a compiled sub-workflow is a nested agent, which + # is what the framework itself calls it. + return "agent" + if parent_span_id is None and instance is not None: + # Any other top-level instrumented call — `query_engine.query()`, + # a bare `llm.chat()` — opens the session and becomes its root + # agent, named after its class. The alternative is emitting leaves + # with no open agent above them, and the dashboard answers that by + # synthesizing a root span that stays `ongoing` forever. + return "agent" + if instance is None: + # A workflow step: the runtime wraps the step function, so there is + # no instance, and its parent is the workflow run span. Checking the + # parent (rather than the `llamaindex.step.*` tags) is what keeps + # this precise — those tags are inherited by every child span, + # including the LLM call inside the step. + parent = self._spans.get(parent_span_id) if parent_span_id else None + if parent is not None and parent.kind == "agent": + return "step" + return "other" + for kind, key in ( + ("tool", "BaseTool"), + ("model", "BaseLLM"), + ("retrieval", "BaseRetriever"), + ("embedding", "BaseEmbedding"), + ): + base = bases.get(key) + if base is not None and isinstance(instance, base): + if kind == "model" and method not in _LLM_METHODS: + return "other" + return kind + return "other" + + # -- agents ----------------------------------------------------------- + + def _start_agent( + self, + span_id: str, + parent_span_id: str | None, + instance: Any, + cls_name: str, + arguments: dict, + ) -> None: + # `FunctionAgent.name` when there is one, the class name otherwise. + # Never the span id: `agent_id` is a LowCardinality column and the + # global dashboard facet. + raw_name = getattr(instance, "name", None) + label = raw_name if isinstance(raw_name, str) and raw_name else cls_name + start_event = arguments.get("start_event") + goal = getattr(start_event, "user_msg", None) or _summarize(start_event) + + identity = self.tracker.start_agent( + span_id, + agent_id=label, + parent_key=parent_span_id, + goal=goal, + **_core.fw_fields( + span_id=span_id, + workflow=cls_name, + agent_name=raw_name if isinstance(raw_name, str) else None, + ), + ) + # `start_agent` always returns a real `agent_id` (it normalizes and falls + # back to the default); the `| None` is only on the shared `Identity`. + self._runs[span_id] = _Run( + span_id=span_id, agent_id=identity.agent_id or _core.DEFAULT_AGENT_ID + ) + + def _end_agent( + self, + span: _Span, + *, + result: Any, + outcome: str, + error: BaseException | None = None, + ) -> None: + run = self._runs.pop(span.span_id, None) + if run is not None: + # Inner-first: a sub-agent still holding the turn is closed (and its + # error count folded into ours) before we decide whether this run + # owns the failure, or it would outlive the workflow that opened it + # and render `ongoing` forever. + self._close_sub_agent(run, outcome=outcome) + # Invariant 4: `agent_end` force-closes open pauses but NOT open + # tools or models. A run that dies holding one leaves the session + # `ongoing` forever. + self._close_all_leaves(run, reason="run_ended") + if error is not None and run.errors == 0: + # Nothing below us reported this, so the run itself owns it. + # If a leaf already did, a second event would double-count on + # `sessionSummary.errorCount`. + self.tracker.emit( + "error", + span.span_id, + parent_key=span.parent_id, + error_type=type(error).__name__, + message=str(error) or type(error).__name__, + **_core.fw_fields(span_id=span.span_id), + ) + elif error is not None: + self.tracker.emit( + "error", + span.span_id, + parent_key=span.parent_id, + error_type=type(error).__name__, + message=str(error) or type(error).__name__, + ) + summary = _summarize(getattr(result, "result", None) if result is not None else None) + if summary is None and error is not None: + # `summary` is a promoted column and `agent_end` is where the + # dashboard reads a run's outcome. Without this a failed run says + # only "failed": the reason lives on the failing step's + # `hook_completed`, which is payload-only, and is gone entirely when + # `steps=False`. The sibling LangChain adapter already does this. + summary = _error_text(error) + self.tracker.end_agent( + span.span_id, + outcome=outcome, + summary=summary, + **_core.fw_fields(span_id=span.span_id), + ) + + # -- AgentWorkflow sub-agents ------------------------------------------- + + def _sub_agent(self, parent: _Span | None, arguments: dict) -> str | None: + """The nested agent an `AgentWorkflow` step belongs to, or `None`. + + `AgentWorkflow` does NOT run its `FunctionAgent`s as nested workflows — + there is one `AgentWorkflow.run` span and the agents are steps inside + it. Read off the span tree alone, a handoff is therefore invisible: a + two-agent crew lands as one `agent_id="AgentWorkflow"` and the names the + user actually facets by (`researcher`, `analyst`) never reach the + column. The framework does tell us, on every `AgentInput`/`AgentSetup`/ + `AgentOutput` the steps are invoked with: `current_agent_name`. + + So each distinct `current_agent_name` opens a nested agent under the + workflow, and a handoff closes the previous one. The name is **sticky**: + `ToolCall` carries no `current_agent_name`, so a `call_tool` step keeps + whichever agent asked for the tool, which is the correct attribution. + + A standalone `FunctionAgent.run` runs those same steps with its own name + in `current_agent_name`, which is already this run's `agent_id` — hence + the `name != root.agent_id` guard, without which every single-agent run + would nest an agent inside an identically-named agent. + """ + if parent is None or parent.kind != "agent" or parent.run_id is None: + return None + root = self._runs.get(parent.run_id) + if root is None: + return None + name = getattr(arguments.get("ev"), "current_agent_name", None) + if isinstance(name, str) and name and name != root.agent_id and name != root.sub_name: + self._close_sub_agent(root) + root.sub_seq += 1 + # Keyed per turn, not per name: an A -> B -> A handoff must not + # reuse the key of the A we already ended. + key = f"{root.span_id}#sub{root.sub_seq}" + identity = self.tracker.start_agent( + key, + agent_id=name, + parent_key=root.span_id, + **_core.fw_fields( + span_id=root.span_id, agent_name=name, workflow=root.agent_id + ), + ) + self._runs[key] = _Run(span_id=key, agent_id=identity.agent_id or name) + root.sub_key, root.sub_name = key, name + return root.sub_key + + def _close_sub_agent(self, root: _Run, *, outcome: str = "success") -> None: + key, root.sub_key, root.sub_name = root.sub_key, None, None + if key is None: + return + sub = self._runs.pop(key, None) + if sub is not None: + self._close_all_leaves(sub, reason="agent_switch") + # The workflow still has to know something below it failed, or + # `_end_agent` would emit a second `error` event for a failure a + # sub-agent's leaf already reported. + root.errors += sub.errors + self.tracker.end_agent( + key, outcome=outcome, **_core.fw_fields(span_id=root.span_id) + ) + + # -- steps ------------------------------------------------------------ + + def _start_step(self, span_id: str, parent_span_id: str | None, method: str, arguments: dict) -> None: + if not self.steps: + return + incoming = arguments.get("ev") + self.tracker.emit( + "hook_triggered", + span_id, + parent_key=parent_span_id, + hook_name=method, + hook_id=span_id, + trigger_event="workflow_step", + input=_summarize(incoming), + **_core.fw_fields( + step=method, + input_event=type(incoming).__name__ if incoming is not None else None, + agent_name=getattr(incoming, "current_agent_name", None), + ), + ) + + def _end_step( + self, + span: _Span, + *, + result: Any, + error: BaseException | None = None, + cancelled: bool = False, + ) -> None: + if not self.steps: + return + run = self._runs.get(span.run_id) if span.run_id else None + if error is not None and run is not None: + run.errors += 1 + if error is not None: + outcome = "failed" + elif cancelled: + # A cancelled step exits with `result=None` and no error, so without + # the mark it reports `success` with an empty output. + outcome = "cancelled" + else: + outcome = "success" + self.tracker.emit( + "hook_completed", + span.span_id, + parent_key=span.parent_id, + hook_name=span.name, + hook_id=span.span_id, + outcome=outcome, + output=_summarize(result), + error=_error_text(error) if error is not None else None, + **_core.fw_fields( + step=span.name, + output_event=type(result).__name__ if result is not None else None, + agent_name=getattr(result, "current_agent_name", None), + ), + ) + + # -- tools ------------------------------------------------------------ + + def _start_tool(self, span_id: str, parent_span_id: str | None, instance: Any, arguments: dict) -> None: + run = self._run_of(span_id) + metadata = getattr(instance, "metadata", None) + tool_name = getattr(metadata, "name", None) or type(instance).__name__ + + # The enclosing `call_tool` step carries the typed `ToolCall`, which is + # where the LLM's own tool id lives. Reusing it keeps our events lined + # up with the provider's. + raw_id = None + parent = self._spans.get(parent_span_id) if parent_span_id else None + if parent is not None and parent.kind == "step": + raw_id = getattr(self._step_input(parent_span_id), "tool_id", None) + call_id = self._tool_call_id(run, raw_id, span_id) + + # A resumed run announces itself by re-entering the tool that paused, + # so `agent_resume`/`human_input` go out BEFORE this attempt's + # `tool_use` — that keeps the paused window (which the graph measures + # from `agent_pause` to `agent_resume`) free of the retry. + if run is not None and run.pauses: + self._resume(run, span_id, parent_span_id) + + kwargs = arguments.get("kwargs") + self._open_leaf( + run, + _Leaf( + kind="tool", + span_id=span_id, + parent_id=parent_span_id, + name=tool_name, + call_id=call_id, + started=time.monotonic(), + ), + ) + self.tracker.emit( + "tool_use", + span_id, + parent_key=parent_span_id, + tool_name=tool_name, + tool_call_id=call_id, + input=kwargs if isinstance(kwargs, dict) else None, + **_core.fw_fields(span_id=span_id, tool_id=raw_id), + ) + + def _step_input(self, span_id: str | None) -> Any: + """The typed workflow event a step was invoked with. + + This is the whole point of reading the span handler rather than the + (dead) agent events: `bound_args.arguments["ev"]` on a step span is the + real `ToolCall` / `AgentInput` / `AgentOutput` object. + """ + return self._step_inputs.get(span_id) if span_id else None + + # -- leaves ----------------------------------------------------------- + + def _open_leaf(self, run: _Run | None, leaf: _Leaf) -> None: + if run is not None: + run.open_leaves[leaf.span_id] = leaf + self._leaf_run[leaf.span_id] = run.span_id + + def _take_leaf(self, span_id: str) -> tuple[_Run | None, _Leaf | None]: + """Detach an open leaf by span id, wherever its run is.""" + run = self._runs.get(self._leaf_run.pop(span_id, "") or "") + if run is None: + return None, None + return run, run.open_leaves.pop(span_id, None) + + def _close_leaf( + self, + span: _Span, + *, + output: Any, + error: BaseException | None = None, + ) -> None: + run, leaf = self._take_leaf(span.span_id) + if leaf is None: + return + if error is not None and run is not None: + run.errors += 1 + self._emit_leaf_close( + leaf, + output=output, + error=_error_text(error) if error is not None else None, + ) + + def _emit_leaf_close( + self, + leaf: _Leaf, + *, + output: Any, + error: str | None, + reason: str | None = None, + ) -> None: + if leaf.kind == "model": + usage, input_tokens, output_tokens = extract_usage(output) + message = getattr(output, "message", None) + content = getattr(message, "content", None) + if content is None: + content = getattr(output, "text", None) + self.tracker.emit( + "model_response", + leaf.span_id, + parent_key=leaf.parent_id, + model=leaf.model, + request_id=leaf.call_id, + role=getattr(getattr(message, "role", None), "value", None), + content=_summarize(content), + input_tokens=input_tokens, + output_tokens=output_tokens, + usage=usage, + error=error, + # Invariant 3: ALWAYS an int. `durationOf` prefers the closing + # event's value over end-start, which is what keeps model + # durations right even when the dashboard's FIFO pairing + # brackets the wrong pair. A float silently NULLs the column. + duration_ms=_core.ms(time.monotonic() - leaf.started), + **_core.fw_fields(span_id=leaf.span_id, closed_by=reason), + ) + return + # tool / retrieval / embedding all close as a tool_result. `duration_ms` + # is auto-computed from the pending `tool_call_id` and is REJECTED if we + # pass it, so it is deliberately absent here. + payload: Any + if leaf.kind == "retrieval": + payload = summarize_nodes(output) if error is None else None + else: + payload = _summarize(getattr(output, "content", None) or output) + self.tracker.emit( + "tool_result", + leaf.span_id, + parent_key=leaf.parent_id, + tool_name=leaf.name, + tool_call_id=leaf.call_id, + output=payload, + error=error, + **_core.fw_fields(span_id=leaf.span_id, closed_by=reason), + ) + + def _close_all_leaves(self, run: _Run, *, reason: str) -> None: + for leaf in list(run.open_leaves.values()): + run.open_leaves.pop(leaf.span_id, None) + self._leaf_run.pop(leaf.span_id, None) + self._emit_leaf_close(leaf, output=None, error=None, reason=reason) + + # -- human in the loop ------------------------------------------------- + + def _pause(self, span: _Span, err: BaseException | None) -> None: + """A `WaitingForEvent` drop: the run is waiting on a human. + + Both pairs are emitted, in this order, because neither alone is enough: + only `agent_pause`/`agent_resume` feeds the graph's paused time, and + only `human_wait`/`human_input` carries the prompt and the pending-human + badge. + """ + run = self._runs.get(span.run_id) if span.run_id else None + waiter = getattr(err, "add", None) + waiter_id = getattr(waiter, "waiter_id", None) or uuid.uuid4().hex + prompt = _summarize(getattr(waiter, "waiter_event", None)) + pause_id = f"{waiter_id}:{uuid.uuid4().hex[:8]}" + + # The tool that paused will be re-run from scratch when the human + # answers, so close its leaf now rather than leaving it open forever. + # This is honest: LlamaIndex really does call the tool twice. + if run is not None: + self._leaf_run.pop(span.span_id, None) + leaf = run.open_leaves.pop(span.span_id, None) + if leaf is not None: + self._emit_leaf_close(leaf, output=None, error=None, reason="human_wait") + run.pauses[pause_id] = waiter_id + + self.tracker.emit( + "human_wait", + span.span_id, + parent_key=span.parent_id, + input_id=pause_id, + prompt=prompt, + reason="workflow is waiting for a human response", + **_core.fw_fields(waiter_id=waiter_id, span_id=span.span_id), + ) + self.tracker.emit( + "agent_pause", + span.span_id, + parent_key=span.parent_id, + pause_id=pause_id, + reason="human_input", + **_core.fw_fields(waiter_id=waiter_id), + ) + + def _resume(self, run: _Run, span_id: str, parent_id: str | None) -> None: + for pause_id, waiter_id in list(run.pauses.items()): + run.pauses.pop(pause_id, None) + self.tracker.emit( + "agent_resume", + span_id, + parent_key=parent_id, + pause_id=pause_id, + reason="human_input", + **_core.fw_fields(waiter_id=waiter_id), + ) + # `response` is left unset: the human's answer arrives as a + # `HumanResponseEvent` sent straight into the workflow context, + # which never reaches the dispatcher. It surfaces on the paired + # `tool_result` instead. + self.tracker.emit( + "human_input", + span_id, + parent_key=parent_id, + input_id=pause_id, + **_core.fw_fields(waiter_id=waiter_id), + ) + + # -- dispatcher events ------------------------------------------------- + + def model_start(self, span_id: str, messages: Any, model_dict: Any, prompt: Any = None) -> None: + with self._lock: + span = self._spans.get(span_id) + run = self._run_of(span_id) + parent_id = span.parent_id if span is not None else None + model = self._model_name(span_id, model_dict) + self._open_leaf( + run, + _Leaf( + kind="model", + span_id=span_id, + parent_id=parent_id, + name=model or "llm", + call_id=span_id, + started=time.monotonic(), + model=model, + ), + ) + self.tracker.emit( + "model_request", + span_id, + parent_key=parent_id, + model=model, + # `request_id` is the LLM span id. It is what pairs the two + # model events in the dashboard's detail panel; no SDK set it + # before this work, so nothing was pairing. + request_id=span_id, + messages=_messages(messages) if self.capture_messages else None, + system=_summarize(prompt) if self.capture_messages else None, + **_core.fw_fields(span_id=span_id), + ) + + def model_end(self, span_id: str, response: Any, error: str | None = None) -> None: + with self._lock: + run, leaf = self._take_leaf(span_id) + if leaf is None: + return + if error is not None and run is not None: + run.errors += 1 + self._emit_leaf_close(leaf, output=response, error=error) + + def retrieval_start(self, span_id: str, query: Any) -> None: + with self._lock: + span = self._spans.get(span_id) + run = self._run_of(span_id) + parent_id = span.parent_id if span is not None else None + name = span.cls if span is not None else "retriever" + self._open_leaf( + run, + _Leaf( + kind="retrieval", + span_id=span_id, + parent_id=parent_id, + name=name, + call_id=span_id, + started=time.monotonic(), + ), + ) + self.tracker.emit( + "tool_use", + span_id, + parent_key=parent_id, + tool_name=name, + tool_call_id=span_id, + input={"query": _summarize(query)}, + **_core.fw_fields(span_id=span_id, kind="retrieval"), + ) + + def retrieval_end(self, span_id: str, nodes: Any) -> None: + with self._lock: + _run, leaf = self._take_leaf(span_id) + if leaf is None: + return + self._emit_leaf_close(leaf, output=nodes, error=None) + + def cancel(self, span_id: str | None) -> None: + """`SpanCancelledEvent` — remember it for the `span_exit` right behind it.""" + if span_id is None: + return + with self._lock: + while len(self._cancelled) >= _MAX_SPANS: + self._cancelled.discard(next(iter(self._cancelled))) + self._cancelled.add(span_id) + + def exception(self, span_id: str | None, exc: Any) -> None: + """`ExceptionEvent` — close whatever leaf that span owns. + + No standalone `error` event: the span that owns the failure reports it, + and the enclosing agent reports it once more only if nothing below it + did. + """ + if span_id is None: + return + with self._lock: + run, leaf = self._take_leaf(span_id) + if leaf is None: + return + if run is not None: + run.errors += 1 + text = _error_text(exc) if isinstance(exc, BaseException) else str(exc) + self._emit_leaf_close(leaf, output=None, error=text) + + def _model_name(self, span_id: str, model_dict: Any) -> str | None: + """`instance.metadata.model_name`, captured when the span opened. + + NOT `model_dict["model"]`: `to_payload()` replaced `to_dict()` in + 0.14.23 and that key no longer exists, so reading it returns None + silently — the bug Traceloop and MLflow are shipping today. + """ + name = self._model_names.get(span_id) + if name: + return name + if isinstance(model_dict, dict): + candidate = model_dict.get("model_name") + if isinstance(candidate, str) and candidate: + return candidate + return None + + # -- reaper ------------------------------------------------------------ + + def sweep(self) -> int: + """Close leaves nobody is going to close. Returns how many. + + A streaming response that is never consumed produces an + `LLMChatStartEvent` with no end, and its span has already exited. Left + alone that is an open `model_request` and a session the dashboard shows + as `ongoing` forever. + """ + closed = 0 + cutoff = time.monotonic() - self.stale_after + with self._lock: + for run in list(self._runs.values()): + for leaf in list(run.open_leaves.values()): + if leaf.started > cutoff: + continue + run.open_leaves.pop(leaf.span_id, None) + self._leaf_run.pop(leaf.span_id, None) + self._emit_leaf_close(leaf, output=None, error=None, reason="stale") + closed += 1 + return closed + + def start_reaper(self) -> None: + if self.reaper_interval <= 0 or self._reaper is not None: + return + thread = threading.Thread( + target=self._reap_loop, name="failproofai_sdk-llamaindex-reaper", daemon=True + ) + self._reaper = thread + thread.start() + + def _reap_loop(self) -> None: + while not self._stop.wait(self.reaper_interval): + _core.call_safely(self.sweep, (), {}, "llama_index.reaper") + + def shutdown(self) -> None: + """Close everything still open, then stop the reaper. Never raises.""" + self._stop.set() + with self._lock: + for run in list(self._runs.values()): + self._close_all_leaves(run, reason="uninstrument") + # Newest first, so a sub-agent closes before the workflow that + # opened it rather than after it. + for span_id in reversed(list(self._runs)): + self.tracker.end_agent(span_id, outcome="cancelled") + self._runs.clear() + self._spans.clear() + self._leaf_run.clear() + self._cancelled.clear() + self._step_inputs.clear() + self._model_names.clear() + self.tracker.reset() + thread = self._reaper + self._reaper = None + if thread is not None and thread.is_alive(): + thread.join(timeout=1.0) + + +# --------------------------------------------------------------------------- +# Framework base classes, imported once and only when asked +# --------------------------------------------------------------------------- + +_BASES: dict[str, Any] = {} + + +def _bases() -> dict[str, Any]: + """The isinstance targets used to classify a span. + + `isinstance` rather than a class-name string: a rename lands as a hard + ImportError at `instrument()` time instead of a classifier that quietly + stops matching. Each import is individually optional, because a partial + LlamaIndex install should cost one span kind, not the adapter. + """ + if _BASES: + return _BASES + targets = { + "Workflow": ("llama_index.core.workflow", "Workflow"), + "BaseTool": ("llama_index.core.tools.types", "BaseTool"), + "BaseLLM": ("llama_index.core.base.llms.base", "BaseLLM"), + "BaseRetriever": ("llama_index.core.base.base_retriever", "BaseRetriever"), + "BaseEmbedding": ("llama_index.core.base.embeddings.base", "BaseEmbedding"), + } + import importlib + + for key, (module_name, attribute) in targets.items(): + try: + _BASES[key] = getattr(importlib.import_module(module_name), attribute) + except Exception: + _compat.warn( + f"failproofai_sdk: llama_index could not resolve {module_name}.{attribute}; " + f"spans of that kind will be recorded as untyped.", + key=f"llama_index:base:{key}", + ) + _BASES[key] = None + return _BASES + + +# --------------------------------------------------------------------------- +# Handlers +# --------------------------------------------------------------------------- + +# The dispatcher dispatches on the concrete event class, so these names ARE the +# API. `test_llama_index.py` asserts every one of them still exists in the +# framework's event modules — a rename would otherwise leave this table looking +# perfectly healthy while recording nothing. +MODEL_START_EVENTS = ("LLMChatStartEvent", "LLMCompletionStartEvent") +MODEL_END_EVENTS = ("LLMChatEndEvent", "LLMCompletionEndEvent", "StreamChatEndEvent") +MODEL_ERROR_EVENTS = ("StreamChatErrorEvent",) +RETRIEVAL_START_EVENTS = ("RetrievalStartEvent",) +RETRIEVAL_END_EVENTS = ("RetrievalEndEvent",) +EMBEDDING_START_EVENTS = ("EmbeddingStartEvent",) +EMBEDDING_END_EVENTS = ("EmbeddingEndEvent",) +EXCEPTION_EVENTS = ("ExceptionEvent",) +# NOT part of `_HANDLED_EVENTS`: this one is dispatched by the workflows runtime +# (`workflows.runtime.types.step_function`), not from +# `llama_index.core.instrumentation.events.*` like every name above it, so the +# drift test that walks those modules cannot cover it. +CANCEL_EVENTS = ("SpanCancelledEvent",) + +_HANDLED_EVENTS = ( + MODEL_START_EVENTS + + MODEL_END_EVENTS + + MODEL_ERROR_EVENTS + + RETRIEVAL_START_EVENTS + + RETRIEVAL_END_EVENTS + + EMBEDDING_START_EVENTS + + EMBEDDING_END_EVENTS + + EXCEPTION_EVENTS +) + +_CLASSES: dict[str, Any] = {} + + +def handler_classes() -> tuple[Any, Any]: + """`(FailproofAIEventHandler, FailproofAISpanHandler)`, built on first use. + + They subclass framework base classes, so they cannot exist at module import + time — and `import failproofai_sdk` must stay free of LlamaIndex. Memoised so + repeated install/uninstall cycles do not rebuild pydantic models. + """ + if _CLASSES: + return _CLASSES["event"], _CLASSES["span"] + + from llama_index_instrumentation.event_handlers.base import BaseEventHandler + from llama_index_instrumentation.span_handlers.base import BaseSpanHandler + from pydantic import PrivateAttr + + class FailproofAIEventHandler(BaseEventHandler): + """Model, retrieval and embedding events. + + `handle` is wrapped in `_core.safe` because the dispatcher's own + `except BaseException: pass` has NO logging: without this, a bug here is + undetectable in production. + """ + + _state: Any = PrivateAttr(default=None) + + def __init__(self, state: Any = None, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._state = state + + @classmethod + def class_name(cls) -> str: + return "FailproofAIEventHandler" + + @_core.safe + def handle(self, event: Any, **kwargs: Any) -> None: + state = self._state + if state is None: + return + name = type(event).__name__ + span_id = getattr(event, "span_id", None) + if span_id is None: + return + if name in MODEL_START_EVENTS: + state.model_start( + span_id, + getattr(event, "messages", None), + getattr(event, "model_dict", None), + prompt=getattr(event, "prompt", None), + ) + elif name in MODEL_END_EVENTS: + state.model_end(span_id, getattr(event, "response", None)) + elif name in MODEL_ERROR_EVENTS: + state.model_end( + span_id, None, error=_summarize(getattr(event, "exception", None)) or name + ) + elif name in RETRIEVAL_START_EVENTS: + state.retrieval_start(span_id, getattr(event, "str_or_query_bundle", None)) + elif name in RETRIEVAL_END_EVENTS: + state.retrieval_end(span_id, getattr(event, "nodes", None)) + elif state.embeddings and name in EMBEDDING_START_EVENTS: + state.retrieval_start(span_id, "embedding") + elif state.embeddings and name in EMBEDDING_END_EVENTS: + state.retrieval_end(span_id, getattr(event, "embeddings", None)) + elif name in EXCEPTION_EVENTS: + state.exception(span_id, getattr(event, "exception", None)) + elif name in CANCEL_EVENTS: + state.cancel(span_id) + + class FailproofAISpanHandler(BaseSpanHandler): + """The span tree: agents, workflow steps, tools, retrievers. + + Note `new_span` takes **`parent_span_id`** while the dispatcher's + `span_enter` takes `parent_id`. Declaring the wrong one here gives a + flat trace and no error at all. + + Every override returns a truthy value so the base class's `span_exit` + can `del self.open_spans[id_]` — returning `None` from + `prepare_to_exit_span` deliberately LEAKS the entry (that is how the + framework parks streaming spans), and returning it from `new_span` would + make the matching `del` raise. + """ + + _state: Any = PrivateAttr(default=None) + + def __init__(self, state: Any = None, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._state = state + + @classmethod + def class_name(cls) -> str: + return "FailproofAISpanHandler" + + @_core.safe + def new_span( + self, + id_: str, + bound_args: Any, + instance: Any = None, + parent_span_id: str | None = None, + tags: dict | None = None, + **kwargs: Any, + ) -> str: + state = self._state + if state is not None: + arguments = getattr(bound_args, "arguments", None) or {} + if "ev" in arguments: + state._step_inputs[id_] = arguments["ev"] + metadata = getattr(instance, "metadata", None) + model_name = getattr(metadata, "model_name", None) + if isinstance(model_name, str) and model_name: + state._model_names[id_] = model_name + state.span_enter(id_, bound_args, instance, parent_span_id, tags) + return id_ + + @_core.safe + def prepare_to_exit_span( + self, + id_: str, + bound_args: Any, + instance: Any = None, + result: Any = None, + **kwargs: Any, + ) -> str | None: + state = self._state + if state is not None: + state.span_exit(id_, bound_args, instance, result) + state._step_inputs.pop(id_, None) + state._model_names.pop(id_, None) + # Truthy tells the base class to `del self.open_spans[id_]`, which + # raises if `new_span` never stored it (it returns None when + # `safe()` swallowed an exception). Returning None here parks the + # span instead — the framework's own mechanism, not a leak. + return id_ if id_ in self.open_spans else None + + @_core.safe + def prepare_to_drop_span( + self, + id_: str, + bound_args: Any, + instance: Any = None, + err: BaseException | None = None, + **kwargs: Any, + ) -> str | None: + state = self._state + if state is not None: + state.span_drop(id_, bound_args, instance, err) + state._step_inputs.pop(id_, None) + state._model_names.pop(id_, None) + return id_ if id_ in self.open_spans else None + + _CLASSES["event"] = FailproofAIEventHandler + _CLASSES["span"] = FailproofAISpanHandler + return FailproofAIEventHandler, FailproofAISpanHandler + + +# --------------------------------------------------------------------------- +# The adapter +# --------------------------------------------------------------------------- + +class _LlamaIndexAdapter: + """Registered as `llama_index`; see `failproofai_sdk.integrations.__init__`.""" + + name = FRAMEWORK + module = "llama_index" + + def __init__(self) -> None: + self.state: _State | None = None + self._handlers: tuple[Any, Any] | None = None + + def install(self, **options: Any) -> None: + instrumentation = _compat.require_module( + "llama_index.core.instrumentation", dist=DIST, extra=EXTRA + ) + _compat.check_version( + FRAMEWORK, + DIST, + minimum=MIN_VERSION, + below=BELOW_VERSION, + reason="0.14.23 replaced to_dict() with to_payload() and is the first " + "release whose workflow spans carry the typed agent events", + ) + if not _compat.probe( + FRAMEWORK, "get_dispatcher", lambda: instrumentation.get_dispatcher + ): + return + + state = _State(**options) + event_cls, span_cls = handler_classes() + event_handler = event_cls(state=state) + span_handler = span_cls(state=state) + + # The ROOT dispatcher. Child dispatchers propagate upward, so one + # registration here sees the whole process. + dispatcher = instrumentation.get_dispatcher() + dispatcher.add_event_handler(event_handler) + dispatcher.add_span_handler(span_handler) + + self.state = state + self._handlers = (event_handler, span_handler) + state.start_reaper() + + def uninstall(self) -> None: + handlers = self._handlers + self._handlers = None + state = self.state + self.state = None + if handlers is not None: + try: + import llama_index.core.instrumentation as instrumentation + + dispatcher = instrumentation.get_dispatcher() + ours = set(id(handler) for handler in handlers) + # IN-PLACE slice assignment. `add_span_handler` does + # `self.span_handlers += [h]`, so a plain `=` rebinds the + # pydantic field and can drop handlers added by someone else. + dispatcher.event_handlers[:] = [ + handler for handler in dispatcher.event_handlers if id(handler) not in ours + ] + dispatcher.span_handlers[:] = [ + handler for handler in dispatcher.span_handlers if id(handler) not in ours + ] + except Exception: + _core.logger.warning( + "failproofai_sdk: could not detach the llama_index handlers", exc_info=True + ) + if state is not None: + # Close every open leaf and agent before we stop listening: a run + # abandoned mid-flight would otherwise render `ongoing` forever. + _core.call_safely(state.shutdown, (), {}, "llama_index.shutdown") + + +adapter = _LlamaIndexAdapter() + +install: Callable[..., None] = adapter.install +uninstall: Callable[..., None] = adapter.uninstall diff --git a/sdk/python/failproofai_sdk/integrations/pydantic_ai.py b/sdk/python/failproofai_sdk/integrations/pydantic_ai.py new file mode 100644 index 000000000..a54eeb402 --- /dev/null +++ b/sdk/python/failproofai_sdk/integrations/pydantic_ai.py @@ -0,0 +1,947 @@ +"""Failproof AI adapter for Pydantic AI, built on `AbstractCapability`. + +Written against **pydantic-ai-slim 2.20.0** (2026-07-29), read out of the +installed package rather than from memory. Every v1 tutorial is wrong for this +version: `Agent(instrument=...)` was **removed in 2.0.0b1**, and +`opentelemetry-api` became a core, non-optional dependency of pydantic-ai-slim. + +Why the capability protocol and not an OTel span processor +---------------------------------------------------------- +`pydantic_ai.capabilities.AbstractCapability` is a full middleware protocol — +`wrap_run`, `wrap_model_request`, `wrap_tool_execute` and friends — and +Pydantic's own `Instrumentation` capability is just another consumer of it with +no privileged access. So `capabilities=[FailproofAI()]`: + +* hands us typed `RunContext` / `ModelResponse` / `RunUsage` objects instead of + stringly-typed span attributes; +* needs no OTel **SDK** on the user's machine (only the API package, which is + already a hard dependency); +* cannot double-count against the user's own tracing, because we are a sibling + of `Instrumentation` rather than a second exporter on the same spans. + +The OTel route would additionally have had to read **both** `gen_ai.usage.*` +(model spans) and `gen_ai.aggregated_usage.*` (run spans) — the latter is a +Pydantic extension that every existing vendor silently drops, which is how +agent-run token counts come out as zero elsewhere. We read `RunUsage` directly +and the question does not arise. + +Adapter shape +------------- +This is **Shape A** end to end: every capability hook is a wrapper whose start +and end sit in one frame, so the adapter can bind Failproof AI identity onto +contextvars for the duration of a run (`failproofai_sdk.session(...)`) and nested +`agent.run()` calls — an agent invoked from inside another agent's tool — nest +themselves with no bookkeeping. `RunTracker` still owns the id/parent/session +resolution and every emit, so `framework` / `framework_version` / +`integration_version` land on **every** event, including `agent_end`. + +One thing Shape A does **not** give you here, and `_open_runs` is the whole of +what this module keeps to cover it: a leaf hook can return *after* the run hook +that contains it. Cancellation is the reliable case — the graph awaits a +`gather` of tool tasks, so the run body unwinds the moment that future is +cancelled while each tool task's own `CancelledError` lands a loop iteration +later. `_close_spans` therefore closes whatever leaves are still open before +`agent_end`, and `_claim_span` / `_claim_run` make the late handler (and a +concurrent `uninstall()`) a no-op rather than a duplicate. + +How it installs +--------------- +There is no supported global capability default in 2.20: `Agent.instrument_all()` +sets `Agent._instrument_default`, which is consulted *only* for the built-in +`Instrumentation` capability, and the one real auto-injection list +(`pydantic_ai.agent._AUTO_INJECT_CAPABILITY_TYPES`) is a private module +constant. So `install()` wraps the public `Agent.__init__` and appends our +capability to its keyword-only `capabilities=` argument, saving the original +function object through `_core.Patcher` so `uninstall()` restores exactly that +object. + +Consequence, and it is worth knowing: an `Agent` **constructed while +instrumented keeps the capability object forever** — we cannot retro-remove +ourselves from an already-built agent. `uninstall()` therefore also flips a +module-level flag that makes every hook a straight pass-through, so an agent +built before `uninstrument()` stops recording rather than recording into a +half-dismantled adapter. By the same token, agents constructed *before* +`instrument()` are not instrumented; construct them after, or pass +`capabilities=[FailproofAI()]` yourself. + +What is deliberately not captured +--------------------------------- +* **Graph nodes** (`UserPromptNode`, `ModelRequestNode`, `CallToolsNode`) get no + events. They are Pydantic AI's own loop machinery, not user-authored steps — + unlike a LangGraph node, which is a `hook_*` pair because the user wrote it. + Everything a node does that is worth seeing is already covered by the model + and tool spans, and `wrap_node_run` would double the row count for nothing. + Not overriding it also keeps `AbstractCapability.has_wrap_node_run` False. +* **`wrap_run_event_stream`** is not overridden either, and that one is a + landmine: overriding it makes `agent.run()` switch itself into streaming mode. + Per-token events are forbidden anyway (a 500-token response would be 500 + stored rows against a 5-lane rail). +""" + +import dataclasses +import logging +import threading +import time +import traceback as _traceback +import uuid +from typing import Any + +import failproofai_sdk +from failproofai_sdk._scopes import _is_cancellation +from failproofai_sdk.integrations import _compat, _core +from failproofai_sdk.integrations._core import ( + RunTracker, + framework_fields, + fw_fields, + ms, + normalize_agent_id, + safe, +) + +logger = logging.getLogger("failproofai_sdk.integrations") + +NAME = "pydantic_ai" +MODULE = "pydantic_ai" +DIST = "pydantic-ai-slim" +EXTRA = "pydantic-ai" + +# 2.0 is a CAPABILITY floor, not a guess: it is the release that removed +# `Agent(instrument=...)` and introduced `AbstractCapability` — the entire +# surface this module is built on. The ceiling is deliberate: without one, a +# clean build after the next major shifts the hook names and this adapter stops +# recording while raising nothing at all. +MIN_VERSION = "2.0" +BELOW_VERSION = "3" +_VERSION_REASON = "2.0 removed Agent(instrument=...) and added the capabilities middleware protocol" + +# This module is only ever imported by `instrument("pydantic_ai")`, so a +# module-level framework import is fine here and nowhere else — `import failproofai_sdk` +# still pulls in zero third-party packages. `require_module` first so the failure +# is the tier-1 ImportError carrying the literal install command, rather than a +# bare `No module named 'pydantic_ai'`. +_compat.require_module("pydantic_ai.capabilities", dist=DIST, extra=EXTRA) + +from pydantic_ai.capabilities import ( # noqa: E402 (deliberately after the probe) + AbstractCapability, + CapabilityOrdering, +) + +# One tracker for the process. `base_fields` is what puts `framework` on every +# single event this adapter emits, agent_end and error included. +_tracker = RunTracker(NAME, base_fields=framework_fields(NAME, DIST)) + +# Flipped by install()/uninstall(). Agents built while instrumented keep the +# capability instance, so this flag — not the patch — is what actually stops the +# recording. Read on every hook, written only under the registry lock. +_enabled = False +_capability: "FailproofAI | None" = None +_patcher = _core.Patcher() + + +# --------------------------------------------------------------------------- +# Control flow that is not failure +# --------------------------------------------------------------------------- + +def _control_flow_types() -> tuple: + """Exceptions Pydantic AI raises to *steer* a run, not to report a failure. + + Resolved by name at import time and tolerant of every one of them being + absent: this list is exactly the kind of thing a minor release renames, and + a missing name must degrade to "treat it as an error" rather than to an + `AttributeError` inside the customer's run. + + `ModelRetry` / `ToolRetryError` / `ToolFailedError` are **not** here on + purpose. They mean an attempt genuinely failed and the model was asked to + try again, which is exactly what a tool span's `error` field is for; the run + itself still ends `success` if the retry works. + """ + import pydantic_ai.exceptions as exceptions + + found = [] + for name in ( + "SkipToolExecution", + "SkipToolValidation", + "SkipModelRequest", + "CallDeferred", + "ApprovalRequired", + ): + candidate = getattr(exceptions, name, None) + if isinstance(candidate, type) and issubclass(candidate, BaseException): + found.append(candidate) + return tuple(found) + + +_CONTROL_FLOW = _control_flow_types() + + +def _is_control_flow(exc: BaseException) -> bool: + return _is_cancellation(type(exc)) or isinstance(exc, _CONTROL_FLOW) + + +def _describe(exc: BaseException) -> str: + text = str(exc) + return f"{type(exc).__name__}: {text}" if text else type(exc).__name__ + + +_TRACEBACK_MARKER = "[older frames truncated]…\n" + + +def _format_traceback(exc: BaseException) -> str: + """The traceback, trimmed from the FRONT if it is too long. + + Every truncation in `_core` keeps the head, which is exactly wrong here: a + traceback's last line is the exception itself, and Pydantic AI's async graph + stack is comfortably longer than the 8KB field limit. Keeping the head would + ship 8KB of framework frames and drop the one line anybody reads. + """ + text = "".join(_traceback.format_exception(type(exc), exc, exc.__traceback__)) + if len(text) <= _core.FIELD_LIMIT: + return text + tail = text[-(_core.FIELD_LIMIT - len(_TRACEBACK_MARKER)):] + return _TRACEBACK_MARKER + tail + + +# --------------------------------------------------------------------------- +# Reading the framework's objects +# --------------------------------------------------------------------------- + +def _run_key(ctx: Any) -> Any: + """A stable per-run key. `run_id` is set for every real run; the fallback + only matters for a synthetic `RunContext` that is not backed by one.""" + run_id = getattr(ctx, "run_id", None) + return ("pydantic_ai.run", run_id if run_id else id(ctx)) + + +def _agent_name(ctx: Any) -> str: + """`agent_id` must stay low-cardinality and human readable. + + Pydantic AI infers `Agent.name` from the assigning call frame on the first + run, so this is normally the variable name (`weather_agent`). + `normalize_agent_id` turns anything id-shaped — or nothing at all — into + `main` and the real id rides in `fw_run_id`. + """ + agent = getattr(ctx, "agent", None) + return normalize_agent_id(getattr(agent, "name", None)) + + +def _text(value: Any) -> str | None: + if value is None: + return None + return value if isinstance(value, str) else str(value) + + +def _model_name(model: Any) -> str | None: + return getattr(model, "model_name", None) if model is not None else None + + +def _render_part(part: Any, capture_content: bool) -> dict: + out: dict[str, Any] = {"part_kind": getattr(part, "part_kind", None) or type(part).__name__} + tool_name = getattr(part, "tool_name", None) + if tool_name: + out["tool_name"] = tool_name + tool_call_id = getattr(part, "tool_call_id", None) + if tool_call_id: + out["tool_call_id"] = tool_call_id + if capture_content: + content = getattr(part, "content", None) + if content is None: + content = getattr(part, "args", None) + if content is not None: + out["content"] = content + return out + + +# Pydantic AI hands `wrap_model_request` the WHOLE conversation on every step, +# so shipping it verbatim makes a 30-step run quadratic in payload size — and +# these are prompts, i.e. the largest strings in the process. `truncate` caps +# each string, but nothing caps the count, so the tail is capped here. The head +# of a long conversation is the least interesting part of a model request. +_MESSAGE_LIMIT = 20 + + +def _render_messages(messages: Any, capture_content: bool) -> "tuple[list[dict] | None, int]": + """(rendered tail, number of older messages omitted).""" + if not messages: + return None, 0 + messages = list(messages) + omitted = max(len(messages) - _MESSAGE_LIMIT, 0) + return [ + { + "kind": getattr(message, "kind", None), + "parts": [_render_part(p, capture_content) for p in getattr(message, "parts", ()) or ()], + } + for message in messages[omitted:] + ], omitted + + +def _render_tools(params: Any, capture_content: bool) -> list[dict] | None: + tools: list[dict] = [] + for group in ("function_tools", "output_tools", "native_tools"): + for tool in getattr(params, group, None) or (): + entry: dict[str, Any] = { + "name": getattr(tool, "name", None) or type(tool).__name__, + "kind": group, + } + if capture_content: + description = getattr(tool, "description", None) + if description: + entry["description"] = description + tools.append(entry) + return tools or None + + +# `RunUsage` renamed `request_tokens`/`response_tokens` to +# `input_tokens`/`output_tokens` in 2.0. Reading the new names only is correct +# for the declared floor, and the anti-drift test asserts they still exist. +_USAGE_KEYS = ( + "input_tokens", + "output_tokens", + "total_tokens", + "cache_read_tokens", + "cache_write_tokens", + "requests", + "tool_calls", +) + + +def _usage_dict(usage: Any) -> dict | None: + """The normalized `usage` blob. Both `event_summary.rs` and + `sessionSummary.ts` fall back to it when the top-level ints are absent.""" + if usage is None: + return None + out = {} + for key in _USAGE_KEYS: + value = getattr(usage, key, None) + if isinstance(value, int) and not isinstance(value, bool) and value: + out[key] = value + return out or None + + +def _token(usage: Any, key: str) -> int | None: + value = getattr(usage, key, None) + return value if isinstance(value, int) and not isinstance(value, bool) else None + + +def _tool_output(result: Any) -> Any: + """What the tool actually produced, unwrapped and JSON-shaped. + + Two lossy renderings happen without this, and `tool_result.output` is the + single most-read field in a tool loop: + + * A tool may return `ToolReturn`, an **envelope** — `return_value` is what + goes back to the model, `content` is an extra user-prompt part and + `metadata` is deliberately never shown to the model at all. The envelope + has no JSON shape, so it rendered as + `ToolReturn(return_value={'answer': 42}, content=…, metadata=…)`: the + answer buried inside a repr, next to a field the model never saw. + * A tool returning a Pydantic model or a dataclass — the documented way to + return structured data — rendered as `Weather(city='Faro', celsius=21)` + rather than `{"city": "Faro", "celsius": 21}`, so nothing downstream can + read a field out of it. + + `_core.truncate` reprs an object with "no JSON shape", which is the right + default for an arbitrary object and wrong for these: they have one, and this + module is the only place that knows it. It is unwrapped with the object's + own `model_dump` / `dataclasses.asdict` rather than `pydantic_core`, because + this package declares no runtime dependencies and `tests/test_zero_dependencies.py` + reads the AST — a guarded import would still be an import. Anything else is + handed through untouched, and every failure falls back to the original value, + which `truncate` then reprs exactly as it does today. + """ + value = getattr(result, "return_value", result) if _is_tool_return(result) else result + if value is None or isinstance(value, (str, bytes, bool, int, float, list, tuple, dict)): + return value + if _is_base_model(value): + for mode in ("json", None): + try: + return value.model_dump(mode=mode) if mode else value.model_dump() + except Exception: + continue + return value + if dataclasses.is_dataclass(value) and not isinstance(value, type): + try: + return dataclasses.asdict(value) + except Exception: + return value + return value + + +def _is_tool_return(value: Any) -> bool: + return getattr(value, "kind", None) == "tool-return" and hasattr(value, "return_value") + + +def _is_base_model(value: Any) -> bool: + """Duck-typed: `model_dump` alone also matches a TypedDict helper or a mock.""" + return hasattr(value, "model_dump") and hasattr(value, "model_fields_set") + + +# --------------------------------------------------------------------------- +# Per-hook state +# --------------------------------------------------------------------------- + +class _RunState: + __slots__ = ("key", "scope", "spans", "closed") + + def __init__(self, key: Any) -> None: + self.key = key + self.scope: Any = None + # Leaf spans this run has opened and not yet closed, newest last. This + # exists because a leaf can outlive its own run — see `_close_spans`. + self.spans: dict[int, "_SpanState"] = {} + self.closed = False + + +class _SpanState: + __slots__ = ("key", "kind", "correlation_id", "started", "extra", "managed") + + def __init__( + self, key: Any, kind: str, correlation_id: str, extra: dict | None = None + ) -> None: + self.key = key + self.kind = kind + self.correlation_id = correlation_id + # perf_counter, not wall clock: a clock adjustment mid-run would + # otherwise produce a negative duration. + self.started = time.perf_counter() + self.extra = extra or {} + # True once this span is registered against a live run, i.e. once the + # run's teardown is able to close it instead of us. + self.managed = False + + def elapsed_ms(self) -> int: + return ms(time.perf_counter() - self.started) + + +# Runs whose `wrap_run` frame is still open, keyed by `_run_key`. The adapter is +# otherwise stateless — this table exists for one reason, and it is the whole of +# `_close_spans` below: a leaf can outlive the run that opened it. +_open_runs: "dict[Any, _RunState]" = {} +_runs_lock = threading.Lock() + +# Bounded for the same reason `RunTracker` is: a `wrap_run` coroutine that is +# garbage-collected before it resumes runs neither of its end branches, and an +# unbounded table of those is a leak in a long-lived server. FIFO — dicts keep +# insertion order. +_MAX_OPEN_RUNS = 10_000 + + +def _register_run(state: _RunState) -> None: + with _runs_lock: + while len(_open_runs) >= _MAX_OPEN_RUNS: + _open_runs.pop(next(iter(_open_runs)), None) + _open_runs[state.key] = state + + +def _claim_run(state: _RunState) -> bool: + """True for the first caller to close this run; False for every later one. + + Two things close a run — `wrap_run`'s own end, and `uninstall()` tearing + down mid-flight — and both must be able to go first. Without this, an + `uninstrument()` called while a run is in flight emitted `agent_end` + (`cancelled`) and the run then emitted a second `agent_end` (`success`) + against a span the dashboard had already closed. + """ + with _runs_lock: + _open_runs.pop(state.key, None) + if state.closed: + return False + state.closed = True + return True + + +def _register_span(span: _SpanState) -> None: + with _runs_lock: + run = _open_runs.get(span.key) + if run is not None: + run.spans[id(span)] = span + span.managed = True + + +def _claim_span(span: _SpanState) -> bool: + """True if this caller owns the span's closing event. + + False only when the run's teardown already emitted it. A span that was never + registered against a run (its `wrap_run` never opened one) is unmanaged and + always closes itself, so an adapter half-failure loses no leaf. + """ + if not span.managed: + return True + with _runs_lock: + run = _open_runs.get(span.key) + if run is None: + return False + return run.spans.pop(id(span), None) is not None + + +def _close_spans(state: _RunState, exc: "BaseException | None") -> None: + """Close every leaf this run opened and did not close, newest first. + + A leaf can outlive its own run, and on the cancellation path it reliably + does. `asyncio.wait_for` cancels the caller's task; the graph is awaiting + a `gather` of tool tasks, so the run body unwinds as soon as that future + is cancelled while each tool task's own `CancelledError` is delivered on + a later loop iteration. `wrap_run` therefore returns *before* + `wrap_tool_execute` does, and the same is true of `wrap_model_request` + when the cancellation lands inside the provider call. Measured against + pydantic-ai 2.32: a `wait_for` timeout produced `agent_end` at + `.565709` and the matching `tool_result` at `.566689`, and a timeout + during a model request put `model_response` after `agent_end` too — the + one thing every other emit in this file is careful never to do, because + the dashboard closes the agent span at `agent_end` and anything after it + is attributed to nothing. Worse, the ambient identity the late leaf + resolves through is unbound by then in some interleavings, and the event + is dropped outright: a `tool_use` with no `tool_result` at all. + + So the run closes them, `_claim_span` stops the real handler emitting a + duplicate when it finally unwinds, and `fw_incomplete` says the leaf did + not report its own outcome. + """ + with _runs_lock: + pending = list(state.spans.values()) + state.spans.clear() + if not pending: + return + # A cancellation is not a failure, exactly as on `agent_end`. + error = None if exc is None or _is_control_flow(exc) else _describe(exc) + for span in reversed(pending): + if span.kind == "tool": + _tracker.emit( + "tool_result", + None, + parent_key=span.key, + tool_name=span.extra.get("tool_name"), + tool_call_id=span.correlation_id, + error=error, + **fw_fields(incomplete=True), + ) + else: + _tracker.emit( + "model_response", + None, + parent_key=span.key, + model=span.extra.get("model"), + role="assistant", + request_id=span.correlation_id, + error=error, + duration_ms=span.elapsed_ms(), + **fw_fields(incomplete=True, streaming=span.extra.get("streaming")), + ) + + +def _close_open_runs() -> None: + """Close every run this adapter still has open, newest first. + + Teardown, not a hot path: `uninstall()` is the only caller. Each run's + leaves close before its `agent_end`, and `_claim_run` makes the run's own + `wrap_run` frame a no-op when it eventually unwinds. + """ + with _runs_lock: + states = list(_open_runs.values()) + for state in reversed(states): + _core.call_safely(_close_run, (state,), {}, f"{__name__}.uninstall") + + +def _close_run(state: _RunState) -> None: + if not _claim_run(state): + return + _close_spans(state, None) + _tracker.end_agent(state.key, outcome="cancelled") + + +# --------------------------------------------------------------------------- +# The capability +# --------------------------------------------------------------------------- + +class FailproofAI(AbstractCapability): + """Record a Pydantic AI agent run into Failproof AI. + + from failproofai_sdk.integrations.pydantic_ai import FailproofAI + agent = Agent("openai:gpt-5.6-sol", capabilities=[FailproofAI()]) + + or, for every agent constructed from now on:: + + failproofai_sdk.instrument("pydantic_ai") + + Event mapping: + + | Pydantic AI | Failproof AI | + |----------------------|-------------------------------------------| + | `wrap_run` | `agent_start` / `agent_end` (+ `error`) | + | `wrap_model_request` | `model_request` / `model_response` | + | `wrap_tool_execute` | `tool_use` / `tool_result` | + | graph nodes | nothing — see the module docstring | + + `session_id` is the run's `conversation_id` (a conversation spanning several + runs is one Failproof AI session, which is the point), unless a hand-written + `failproofai_sdk.session(...)` / `failproofai_sdk.agent(...)` is already open — that always + wins, so mixing the manual API with this adapter produces one tree, not two. + """ + + def __init__( + self, + *, + capture_content: bool = True, + session_id: str | None = None, + id: str | None = None, + ) -> None: + # `AbstractCapability` is a `@dataclass(init=False)`, so it contributes + # class-level defaults (`id`, `defer_loading`, …) and no `__init__` to + # chain to; assigning the ones we care about is the supported pattern. + self.capture_content = capture_content + self.session_id = session_id + self.id = id + + def get_ordering(self) -> CapabilityOrdering: + """Outermost, so our spans bracket every other capability's work. + + The built-in `Instrumentation` declares the same tier; ties break on the + user's list order, which is fine — we are not exchanging state with it. + """ + return CapabilityOrdering(position="outermost") + + # -- run -------------------------------------------------------------- + + async def wrap_run(self, ctx, *, handler): + if not _enabled: + return await handler() + state = self._begin_run(ctx) + try: + result = await handler() + except BaseException as exc: + # Note the structure: the framework's call sits in exactly one + # `try`, whose only job is to re-raise. Everything of ours is + # outside it and inside `safe()`, so nothing we do can change what + # the run returns or raises. + self._end_run(state, None, exc) + raise + self._end_run(state, result, None) + return result + + @safe + def _begin_run(self, ctx) -> _RunState: + key = _run_key(ctx) + ambient = failproofai_sdk.current() + # An already-bound session wins. Handing `start_agent` an explicit + # session_id would otherwise split a hand-written outer scope's run into + # a second session. + session_id = self.session_id or ( + None if ambient.session_id else (getattr(ctx, "conversation_id", None) or getattr(ctx, "run_id", None)) + ) + identity = _tracker.start_agent( + key, + agent_id=_agent_name(ctx), + session_id=session_id, + goal=_text(getattr(ctx, "prompt", None)) if self.capture_content else None, + **fw_fields( + run_id=getattr(ctx, "run_id", None), + conversation_id=getattr(ctx, "conversation_id", None), + model=_model_name(getattr(ctx, "model", None)), + metadata=getattr(ctx, "metadata", None), + ), + ) + state = _RunState(key) + _register_run(state) + try: + scope = failproofai_sdk.session(identity.session_id, agent_id=identity.agent_id) + scope.__enter__() + state.scope = scope + except Exception: # pragma: no cover - two contextvar sets cannot fail + # Deliberately not re-raised: the agent is already open, and the + # state we return is what closes it. A lost contextvar binding costs + # nesting for a nested run; a lost `agent_end` costs a session that + # renders `ongoing` forever. + logger.debug("failproofai_sdk: could not bind pydantic-ai run identity", exc_info=True) + return state + + @safe + def _end_run(self, state: "_RunState | None", result: Any, exc: "BaseException | None") -> None: + if state is None: + return + if not _claim_run(state): + # `uninstall()` tore this run down already and emitted its agent_end. + # A second one would close a span the dashboard has already closed. + if state.scope is not None: + state.scope.__exit__(None, None, None) + state.scope = None + return + try: + # Strictly before agent_end, for the same reason the `error` event + # below is: the dashboard closes the agent span at agent_end. + _core.call_safely(_close_spans, (state, exc), {}, f"{__name__}.close_spans") + usage = _usage_dict(getattr(result, "usage", None)) if result is not None else None + if exc is None: + outcome = "success" + summary = _text(getattr(result, "output", None)) if self.capture_content else None + elif _is_control_flow(exc): + # A cancellation is not an error: it must not pollute the Errors + # surface, so no `error` event and `outcome="cancelled"`. + outcome, summary = "cancelled", None + else: + outcome, summary = "failed", None + # Strictly BEFORE agent_end — the dashboard closes the agent span + # at agent_end and anything after it is attributed to nothing. + # This is the one place a standalone `error` event is right: the + # failure escaped the run, so no leaf span owns it. A tool or + # model failure that the loop recovered from is reported only on + # its own span and never reaches here. + _tracker.emit( + "error", + state.key, + error_type=type(exc).__name__, + message=str(exc), + traceback=_format_traceback(exc), + ) + # "failed", never "failure": the server counts only + # error|failed|timeout|rejected as a failure. + _tracker.end_agent(state.key, outcome=outcome, summary=summary, usage=usage) + finally: + if state.scope is not None: + state.scope.__exit__(None, None, None) + state.scope = None + + # -- model ------------------------------------------------------------ + + async def wrap_model_request(self, ctx, *, request_context, handler): + if not _enabled: + return await handler(request_context) + state = self._begin_model(ctx, request_context) + try: + response = await handler(request_context) + except BaseException as exc: + self._end_model(ctx, state, None, exc) + raise + self._end_model(ctx, state, response, None) + return response + + @safe + def _begin_model(self, ctx, request_context) -> _SpanState: + # `request_id` is generated here and carried onto the response. The + # dashboard pairs model events FIFO per agent_id when it is absent, which + # mis-pairs the moment two model calls overlap. + state = _SpanState(_run_key(ctx), "model", uuid.uuid4().hex) + model = getattr(request_context, "model", None) + state.extra["model"] = _model_name(model) + # Carried onto the response because that is the event `duration_ms` + # rides on — see the note there. + state.extra["streaming"] = getattr(request_context, "streaming", None) + _register_span(state) + messages, omitted = _render_messages( + getattr(request_context, "messages", None), self.capture_content + ) + _tracker.emit( + "model_request", + None, + parent_key=state.key, + model=state.extra["model"], + system=getattr(model, "system", None), + messages=messages, + tools=_render_tools( + getattr(request_context, "model_request_parameters", None), self.capture_content + ), + request_id=state.correlation_id, + **fw_fields( + run_step=getattr(ctx, "run_step", None), + streaming=getattr(request_context, "streaming", None), + model_id=getattr(request_context, "model_id", None), + messages_omitted=omitted or None, + ), + ) + return state + + @safe + def _end_model(self, ctx, state: "_SpanState | None", response: Any, exc: "BaseException | None") -> None: + if state is None or not _claim_span(state): + return + usage = getattr(response, "usage", None) if response is not None else None + error = None + if exc is not None and not _is_control_flow(exc): + error = _describe(exc) + _tracker.emit( + "model_response", + None, + parent_key=state.key, + model=_model_name(response) or state.extra.get("model"), + stop_reason=getattr(response, "finish_reason", None), + input_tokens=_token(usage, "input_tokens"), + output_tokens=_token(usage, "output_tokens"), + content=( + [_render_part(p, True) for p in getattr(response, "parts", ()) or ()] + if response is not None and self.capture_content + else None + ), + role="assistant", + request_id=state.correlation_id, + error=error, + # Always an int, always present. `duration_ms` is not guarded on + # model_response and the dashboard prefers the closing event's value + # over end-start, which is what keeps model durations honest even + # when FIFO pairing brackets the wrong pair. A float would silently + # NULL the column. + # + # On a STREAMED request this number is the whole `async with + # agent.run_stream(...)` block, consumer included, and not the model + # call: Pydantic AI hands `wrap_model_request`'s handler its + # `ModelResponse` only once the caller leaves that block, and + # `after_model_request` fires later still (measured: 818.7ms vs + # 317.6ms for the drain). There is no earlier hook — `stream_text` + # is the caller's own loop and `wrap_run_event_stream` cannot be + # overridden here without switching `agent.run()` into streaming + # mode. Measured against the gateway, 1.5s of `asyncio.sleep` in the + # consumer moved a 3294ms response to 4677ms. So the number cannot + # be made honest, only made *identifiable*: `fw_streaming` rides on + # the response as well as the request, so a latency percentile can + # exclude the rows where it is true rather than quietly averaging + # somebody's UI render time into the model's p95. + duration_ms=state.elapsed_ms(), + usage=_usage_dict(usage), + **fw_fields( + provider=getattr(response, "provider_name", None), + provider_response_id=getattr(response, "provider_response_id", None), + run_step=getattr(ctx, "run_step", None), + streaming=state.extra.get("streaming"), + ), + ) + + # -- tools ------------------------------------------------------------ + + async def wrap_tool_execute(self, ctx, *, call, tool_def, args, handler): + if not _enabled: + return await handler(args) + state = self._begin_tool(ctx, call, tool_def, args) + try: + result = await handler(args) + except BaseException as exc: + self._end_tool(state, None, exc) + raise + self._end_tool(state, result, None) + return result + + @safe + def _begin_tool(self, ctx, call, tool_def, args) -> _SpanState: + # The framework's own id, verbatim: it is what appears in the provider's + # logs, and inventing a synthetic one would destroy that correspondence. + # `duration_ms` on tool_result is auto-computed by the SDK from this key, + # so tool_use and tool_result must agree on it exactly. + tool_call_id = getattr(call, "tool_call_id", None) or uuid.uuid4().hex + tool_name = getattr(call, "tool_name", None) or getattr(tool_def, "name", None) or "tool" + state = _SpanState(_run_key(ctx), "tool", tool_call_id, {"tool_name": tool_name}) + _register_span(state) + _tracker.emit( + "tool_use", + None, + parent_key=state.key, + tool_name=tool_name, + tool_call_id=tool_call_id, + input=args if self.capture_content and isinstance(args, dict) else None, + **fw_fields( + tool_kind=getattr(tool_def, "kind", None), + run_step=getattr(ctx, "run_step", None), + ), + ) + return state + + @safe + def _end_tool(self, state: "_SpanState | None", result: Any, exc: "BaseException | None") -> None: + if state is None or not _claim_span(state): + return + error = None + if exc is not None and not _is_control_flow(exc): + error = _describe(exc) + # No `error` event here, ever. A tool failure the agent loop catches is + # not a run-level error, and one that escapes is reported exactly once, + # by `_end_run`. + _tracker.emit( + "tool_result", + None, + parent_key=state.key, + tool_name=state.extra.get("tool_name"), + tool_call_id=state.correlation_id, + output=_tool_output(result) if self.capture_content else None, + error=error, + ) + + +# --------------------------------------------------------------------------- +# install / uninstall +# --------------------------------------------------------------------------- + +def _inject(kwargs: dict) -> None: + capability = _capability + if capability is None: + return + existing = kwargs.get("capabilities") + items = list(existing) if existing else [] + if any(isinstance(item, FailproofAI) for item in items): + # Somebody passed `capabilities=[FailproofAI()]` explicitly. Theirs wins; + # two of us would double every event. + return + items.append(capability) + kwargs["capabilities"] = items + + +def _wrap_init(original): + """`Agent.__init__` with our capability appended to `capabilities=`. + + Not `_core.wrap_callable`: that shape deliberately cannot touch the wrapped + callable's arguments (its whole guarantee is that the user's call is + untouched), and injecting a capability is precisely an argument mutation. + The `Patcher` still owns the save/restore and the "somebody patched on top + of us" check, so the install discipline is unchanged. + """ + + def _failproofai_agent_init(self, *args, **kwargs): + _core.call_safely(_inject, (kwargs,), {}, f"{__name__}.Agent.__init__") + return original(self, *args, **kwargs) + + _failproofai_agent_init.__name__ = getattr(original, "__name__", "__init__") + _failproofai_agent_init.__qualname__ = getattr(original, "__qualname__", "Agent.__init__") + _failproofai_agent_init.__doc__ = getattr(original, "__doc__", None) + return _failproofai_agent_init + + +class _Adapter: + name = NAME + module = MODULE + + def install(self, **options: Any) -> None: + """Append `FailproofAI()` to every `Agent` constructed from now on. + + Unknown options are ignored rather than rejected: `failproofai_sdk.instrument()` + with no framework name fans the same `**options` out to every detected + adapter, so a keyword meant for LangChain must not take this one down. + """ + global _enabled, _capability + + _compat.check_version( + NAME, DIST, minimum=MIN_VERSION, below=BELOW_VERSION, reason=_VERSION_REASON + ) + + from pydantic_ai import Agent + + _capability = FailproofAI( + capture_content=bool(options.get("capture_content", True)), + session_id=options.get("session_id"), + ) + _patcher.patch(Agent, "__init__", _wrap_init(Agent.__init__)) + _enabled = True + + def uninstall(self) -> None: + global _enabled, _capability + + _enabled = False + _capability = None + _patcher.restore_all() + # Close every agent still open, so a session torn down mid-run does not + # render `ongoing` forever. Runs this adapter opened go through + # `_close_run` — leaves first, and `_claim_run` so the run's own + # `wrap_run` frame does not emit a second `agent_end` when it finally + # unwinds. `close_open_agents()` stays as the backstop for a key that + # reached the tracker without reaching `_open_runs`. + _close_open_runs() + _tracker.close_open_agents() + _tracker.reset() + + +adapter = _Adapter() +install = adapter.install +uninstall = adapter.uninstall diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 30699d6fc..72715a1a9 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -44,7 +44,28 @@ Source = "https://github.com/FailproofAI/failproofai" # `tomli` is `tomllib`'s backport: 3.10 has no stdlib TOML parser, and that test # reads this file to prove the promise, so on 3.10 it needs one rather than # skipping the check on the oldest interpreter we advertise. -dev = ["pytest>=7", "tomli>=2; python_version<'3.11'"] +dev = ["pytest>=7", "tomli>=2; python_version<'3.11'", "pytest-asyncio>=1.3,<2"] + +# Convenience aliases that pull the FRAMEWORK in. The adapter code always ships +# in the base wheel and lazy-imports, so these gate nothing on our side — most +# users already have the framework and will never install an extra. +# +# Each floor is a CAPABILITY floor with a stated reason, not a guess, and each +# ceiling is deliberate: without one, a clean build a year from now pulls the +# next major, the callback API shifts, and the adapter stops receiving events +# while raising nothing. +# +# NEVER write `failproofai-sdk[...]` inside an extra, and there is deliberately +# no `[all]`: an extra that installs four agent frameworks at once is a resolver +# problem handed to somebody who wanted a telemetry library. +langchain = ["langchain-core>=1.4.7,<2"] # langgraph 1.2's own floor +langgraph = ["langgraph>=1.2,<2"] # first release with GraphCallbackHandler +crewai = [ + "crewai>=1.13,<2", # started_event_id + normalized usage + "onnxruntime>=1.14,<1.24; python_version < '3.11'", # 1.24+ dropped CPython 3.10 wheels +] +llamaindex = ["llama-index-core>=0.14.23,<0.15"] +pydantic-ai = ["pydantic-ai-slim>=2.0,<3"] # 2.0 removed Agent(instrument=...) [tool.setuptools.dynamic] version = { attr = "failproofai_sdk._version.__version__" } @@ -58,3 +79,8 @@ failproofai_sdk = ["py.typed"] [tool.pytest.ini_options] testpaths = ["tests"] +# `framework` marks a suite that needs a real agent framework installed. Those +# skip on a plain `pytest` run and are exercised in an environment that has the +# frameworks — registering it here is what keeps an unknown-mark warning from +# training everyone to ignore warnings. +markers = ["framework: needs a real agent framework installed"] diff --git a/sdk/python/skill/SKILL.md b/sdk/python/skill/SKILL.md index e2e78343f..ec362638f 100644 --- a/sdk/python/skill/SKILL.md +++ b/sdk/python/skill/SKILL.md @@ -136,10 +136,18 @@ Full field-by-field catalog: `references/events.md`. Work with these; none of them raise, so none of them show up in testing. -- **There is no ambient session.** No decorator, no context manager, no - contextvar, no `set_session()`. Every one of the 13 methods takes `session_id` - and `agent_id` as required keyword args, and nothing propagates them for you. - This is what §4 exists to solve. +- **There IS an ambient session, and it is the ergonomic path.** `session()`, + `agent()` and `tool_call()` bind identity on contextvars, so `session_id` and + `agent_id` are optional on all 15 event methods — omitted, they resolve from + the enclosing scope. `current()` reads it; `propagate(fn)` carries it into a + new thread, which contextvars do NOT do on their own. + + This section said the opposite until the scopes existed, and the reference + integration shipped a contextvars wrapper as markdown for customers to paste + into their own code. That is now in the package. + + Nothing bound and nothing passed raises `TypeError` naming the fix — never a + silent emit, because ingest skips an event with no session and answers `200`. - **`configure()` is optional, and every call restates all of it.** It is keyword-only with exactly three settings: @@ -214,12 +222,30 @@ Work with these; none of them raise, so none of them show up in testing. silently. **`SIGTERM` deserves its own line, because it is not exotic — it is every - rolling deploy**, every `docker stop`, every Kubernetes eviction. Python's - default handler exits, so `atexit` *does* run; but if your app installs its own - handler and calls `os._exit`, or takes longer than the grace period, the queue - dies with it. What is in flight at shutdown is disproportionately `agent_end`, - so runs never close and never reach the evaluator. If you handle `SIGTERM`, - flush before you exit. + rolling deploy**, every `docker stop`, every Kubernetes eviction, and every + plain `kill`. CPython installs **no** handler for it: `signal.getsignal(SIGTERM)` + is `SIG_DFL`, the OS terminates the process where it stands, and **`atexit` + does not run**. Whatever is queued is gone — and what is in flight at shutdown + is disproportionately `agent_end`, so runs never close and never reach the + evaluator. The 0.5s flush interval is what bounds the loss, not the exit path. + + So if your process can receive `SIGTERM`, handle it — the SDK will not install + a handler in your process behind your back: + + import signal, sys, failproofai_sdk + + def _flush_and_exit(signum, frame): + failproofai_sdk._writer.flush_now() + sys.exit(128 + signum) + + signal.signal(signal.SIGTERM, _flush_and_exit) + + (`sys.exit` here rather than `os._exit`: it unwinds, so any `agent()` scope + still open emits its `agent_end` before the flush. That scope closes + `outcome="failed"` with an `error` naming `SystemExit`, because an evicted run + did not finish — which is the thing you want to be able to see.) `SIGKILL`, + `os._exit` and a container OOM cannot be handled by anything, and drop the + queue silently. ## 4. Write it @@ -227,7 +253,7 @@ Threading `session_id` and `agent_id` through every call site by hand is the thi that makes integrations ugly and abandoned. Don't. Bind identity once per run and let the call sites read it. -`references/integration.md` has the canonical `contextvars` wrapper — one small +`references/frameworks.md` covers the four adapters. `references/integration.md` has the hand-written wrapper — one small module, correct under `asyncio` and threads, adaptable to any codebase — plus worked shapes for a tool dispatcher, an LLM client wrapper, and framework-specific callback layers. Read it before writing your own; the naive version (a module diff --git a/sdk/python/skill/references/events.md b/sdk/python/skill/references/events.md index 74ec91fb0..c659608df 100644 --- a/sdk/python/skill/references/events.md +++ b/sdk/python/skill/references/events.md @@ -1,8 +1,13 @@ # Event catalog -Every method lives on `failproofai_sdk.event`, is **keyword-only**, returns `None`, and -requires `session_id: str` and `agent_id: str`. Nothing here blocks or does I/O — -the call queues the event and returns. +Every method lives on `failproofai_sdk.event`, is **keyword-only**, and returns `None`. +Nothing here blocks or does I/O — the call queues the event and returns. + +`session_id` and `agent_id` are optional on all fifteen: omit them and they come +from the enclosing `failproofai_sdk.session()` / `failproofai_sdk.agent()` scope. Pass them and +your value wins. Omit `session_id` with nothing bound and you get a `TypeError` +naming the fix; `agent_id` falls back to `"main"` and never raises. See +`integration.md`. Emit only what fits the agent. There is no requirement to use every type, and no penalty for skipping one — except `agent_start`, without which the session does @@ -10,7 +15,8 @@ not exist at all. ## The 15 events -Columns: **Required** is beyond the universal `session_id` + `agent_id`. +Columns: **Required** is beyond `session_id` + `agent_id`, which every event +carries and which you rarely pass by hand. *Optional fields are omitted from the record entirely when left unset* — they are not written as `null`. @@ -22,8 +28,8 @@ not written as `null`. | `agent_resume` | `pause_id` | `reason`, `user_id` | Emit **instead of a second `agent_start`** when a paused agent continues. `duration_ms` auto-computed — how long it was paused. | | `tool_use` | `tool_name`, `tool_call_id` | `input` | Starts the duration clock for this `tool_call_id`. | | `tool_result` | `tool_name`, `tool_call_id` | `output`, `error` | `duration_ms` auto-computed from the matching `tool_use`. | -| `model_request` | — | `model`, `messages`, `system`, `tools` | | -| `model_response` | — | `model`, `stop_reason`, `input_tokens`, `output_tokens`, `content`, `role` | Token counts drive spend reporting. | +| `model_request` | — | `model`, `messages`, `system`, `tools`, `request_id` | `request_id` is what pairs this with its response. | +| `model_response` | — | `model`, `stop_reason`, `input_tokens`, `output_tokens`, `content`, `role`, `request_id`, `error` | Token counts drive spend reporting. `error` marks a failed call — a 429 or a timeout — without a separate `error` event. | | `error` | `error_type`, `message` | `traceback` | Always counts as an error, whatever else is set. | | `hook_triggered` | `hook_name`, `hook_id` | `trigger_event`, `input` | Starts the clock for this `hook_id`. | | `hook_completed` | `hook_name`, `hook_id` | `outcome`, `output`, `error` | `duration_ms` auto-computed. Same `outcome` rule as `agent_end`. | @@ -69,25 +75,44 @@ on `tool_result`, `hook_completed`, `human_input`, and `agent_resume`, and passi it to *those four* raises `ValueError`. On the other eleven there is no guard: `agent_end(..., duration_ms=5)` is accepted and stored as an ordinary custom field. Don't. -**Correlation is one flat, process-wide map — not one per type.** The SDK keeps -open `tool_call_id` and `hook_id` values in a single dict keyed by the bare id. -Consequences, in order of how much they hurt: +The one place you *should* pass it is **`model_response`**, where nothing computes +it for you. Time the call yourself and pass whole milliseconds as an **`int`** — +`round(seconds * 1000)`. A float is dropped on the way in and the duration comes +out empty, with nothing anywhere saying so. + +**Set `request_id` on both model events.** The same value on the `model_request` +and its `model_response` — a `uuid4().hex`, or the provider's own request id if it +gives you one, so your events line up with your provider logs. It links the pair +in the event detail view. + +It does **not** yet drive the timeline rail or the model latency reports: those +still match concurrent LLM calls (a parallel fan-out, a sub-agent, a retry racing +its original) oldest-first, and will keep bracketing the wrong pairs. **Always +pass `duration_ms` on the `model_response` too** — that is what keeps the reported +duration right even when the bracketing is wrong. + +**Correlation is a process-wide map keyed by your ids.** The SDK holds open +starts there until their matching end arrives. Consequences, in order of how much +they hurt: -- **`tool_call_id` and `hook_id` collide with each other.** A - `hook_completed(hook_id="x")` will pair with a pending `tool_use(tool_call_id="x")` - and emit a confident, wrong `duration_ms` spanning two unrelated events. Your - ids must be unique across all concurrent runs **and across both namespaces**. - **Per-run counters are unsafe.** `call_1`, `call_2` — common in home-grown loops — collide across overlapping runs. The failure is not the missing duration the docs might lead you to expect; it is a *plausible wrong number attributed to the wrong run*, which is worse. Reuse your framework's id (Anthropic and OpenAI - tool-call ids are globally unique), or a `uuid4`. -- **`input_id` and `pause_id` are the exceptions**: both are scoped per - session/agent (namespaced internally), so `human_wait`/`human_input` and - `agent_pause`/`agent_resume` cannot collide across runs — or with the flat - `tool_call_id`/`hook_id` namespace. + tool-call ids are globally unique), or a `uuid4`. `failproofai_sdk.tool_call()` + generates a `uuid4` for you. +- **`tool_call_id` and `hook_id` no longer collide with each other.** They live in + separate namespaces, so a `hook_completed(hook_id="x")` cannot pair with a + pending `tool_use(tool_call_id="x")`. Each still has to be unique *within its + own namespace*, across every concurrent run in the process. (If you have read + older guidance saying one shared id space, that was true and is not any more — + the ids are namespaced by the SDK, so nothing on your side changes.) +- **`input_id` and `pause_id` are scoped per session/agent**, so + `human_wait`/`human_input` and `agent_pause`/`agent_resume` cannot collide + across runs at all. - **The pair must happen in the same process.** A `tool_use` in one worker and a - `tool_result` in another produces two unpaired events and no duration. + `tool_result` in another produces two unpaired events and no duration. The same + goes for a human pause that resumes in a different process. - **The map is capped at 10,000 and evicts oldest-first**, so a long-running process that leaks orphaned starts can silently lose the duration on legitimate later pairs. @@ -109,7 +134,7 @@ whatever optional and custom fields you set. ```json {"timestamp": "2026-07-17T09:15:22.123456Z", "session_id": "run-001", "agent_id": "planner", "type": "tool_use", "tool_name": "web_search", "tool_call_id": "toolu_01", "environment": "production", "input": {"query": "latest AI research"}} -{"timestamp": "2026-07-17T09:15:23.456789Z", "session_id": "run-001", "agent_id": "planner", "type": "tool_result", "tool_name": "web_search", "tool_call_id": "toolu_01", "environment": "production", "output": {"results": ["..."]}, "duration_ms": 1333.6} +{"timestamp": "2026-07-17T09:15:23.456789Z", "session_id": "run-001", "agent_id": "planner", "type": "tool_result", "tool_name": "web_search", "tool_call_id": "toolu_01", "environment": "production", "output": {"results": ["..."]}, "duration_ms": 1334} ``` **Parse it; don't grep it.** The whitespace above is what today's writer happens @@ -134,8 +159,10 @@ sid = "run-001" failproofai_sdk.event.agent_start(session_id=sid, agent_id="planner", goal="answer the user's question") try: failproofai_sdk.event.model_request(session_id=sid, agent_id="planner", model="claude-opus-4-8", + request_id="req-1", messages=[{"role": "user", "content": "..."}]) failproofai_sdk.event.model_response(session_id=sid, agent_id="planner", model="claude-opus-4-8", + request_id="req-1", duration_ms=812, stop_reason="tool_use", input_tokens=1200, output_tokens=95) failproofai_sdk.event.tool_use(session_id=sid, agent_id="planner", @@ -163,7 +190,22 @@ Note `outcome="failed"` in the `except` — not `"failure"`, which silently read a non-failure. Threading `sid` through by hand like this is fine for one function and miserable -across a real codebase. See `integration.md`. +across a real codebase — which is why you do not have to. The same run, with the +scopes doing the identity and the bracketing: + +```python +with failproofai_sdk.agent("planner", session_id="run-001", goal="answer the user's question"): + failproofai_sdk.event.model_request(model="claude-opus-4-8", request_id="req-1", messages=[...]) + failproofai_sdk.event.model_response(model="claude-opus-4-8", request_id="req-1", duration_ms=812, + stop_reason="tool_use", input_tokens=1200, output_tokens=95) + with failproofai_sdk.tool_call("web_search", input={"query": "..."}) as t: + t.output = {"results": ["..."]} +``` + +`agent_start`/`agent_end`, the `error` event, `outcome="failed"` and the tool +timing all come for free, including on the exception path. See `integration.md`; +on a supported framework, `frameworks.md` gets you the same events with no call +sites at all. ## Pausing and resuming — not ending diff --git a/sdk/python/skill/references/frameworks.md b/sdk/python/skill/references/frameworks.md new file mode 100644 index 000000000..5b85c6679 --- /dev/null +++ b/sdk/python/skill/references/frameworks.md @@ -0,0 +1,300 @@ +# Framework integrations + +If the agent runs on LangChain/LangGraph, CrewAI, LlamaIndex or Pydantic AI, you +do not write the instrumentation — you turn it on. The adapters ship inside the +SDK wheel and are imported only when you ask for them. + +```python +import failproofai_sdk +from langgraph.graph import StateGraph # import your framework FIRST + +failproofai_sdk.configure(environment="production") +failproofai_sdk.instrument() # every supported framework already imported + +graph.invoke({"messages": [...]}) # sessions, tools, models, errors all appear +``` + +No call site changes. `graph.invoke()`, `crew.kickoff()`, `await workflow.run()` +and `await agent.run()` are recorded exactly as they are written today. + +## `instrument()` / `uninstrument()` + +```python +failproofai_sdk.instrument() # auto-detect +failproofai_sdk.instrument("crewai") # exactly one +failproofai_sdk.instrument("langchain", session_id=request_id, capture_content=False) +failproofai_sdk.uninstrument() # put everything back +``` + +- **Auto-detect reads what is already imported, not what is installed.** This is + deliberate — a library that imports LangChain to find out whether you use it + costs you a second of startup for nothing. The consequence is an ordering rule: + `configure()` → import the framework → `instrument()` → run. Calling + `instrument()` too early instruments nothing and returns `()`, silently. +- **It returns the names it newly instrumented**, as a tuple. Assert on it in + startup code if you want a loud failure: `assert failproofai_sdk.instrument()`. +- **Instrumenting something already active is a no-op** returning `()`. Calling + it from two code paths, or from a reloading dev server, cannot double-record. +- **An unknown name raises `ValueError` listing the valid ones.** A typo that + silently records nothing is the worst available outcome, so this one is loud. + Accepted spellings: `langchain` (aliases `langgraph`, `langchain_core`), + `crewai`, `llama_index` (`llamaindex`, `llama-index`), `pydantic_ai` + (`pydantic-ai`, `pydanticai`). +- **One adapter failing does not cost you the others.** With no argument, an + adapter whose install fails is logged and skipped and the rest still install. +- **`uninstrument()` never raises**, restores the original objects it replaced, + and closes anything still open with `outcome="cancelled"` so teardown does not + leave a session reported as ongoing forever. + +Options are keyword arguments to `instrument()`. The same dict reaches every +adapter, so an option meant for one is ignored by the others rather than raising: + +| Option | Applies to | Effect | +|---|---|---| +| `session_id=` | langchain, crewai, pydantic_ai | Pin every run to this session id. Use it when your service already has a per-request id. | +| `capture_content=False` | langchain, pydantic_ai | Drop prompts, messages and outputs; keep structure, durations and token counts. | +| `capture_messages=False` | llama_index | The same, for LlamaIndex. | +| `include_chains=("rag",)` | langchain | Record these intermediate chains by name. Default: none — see below. | +| `graph_callbacks=False` | langchain | Turn off LangGraph interrupt/resume wiring. Default on. | +| `steps=False` | llama_index | Stop emitting a hook pair per workflow step. Default on. | +| `embeddings=True` | llama_index | Record embedding calls. Default off — they are high volume and low signal. | + +## The rule the mappings follow + +> A framework construct becomes an Failproof AI **agent** if and only if it owns an +> LLM decision loop and has its own goal. Everything else with a start and an end +> becomes the closest kind of leaf. + +| Kind | Gets | Examples | +|---|---|---| +| owns a decision loop | `agent_start` / `agent_end` | the top-level graph, crew or workflow, and anything the framework itself calls an agent — a compiled subgraph, a CrewAI `Agent` role, a LlamaIndex `FunctionAgent` | +| something the agent *calls* | `tool_use` / `tool_result` | function tools, retrievers, memory and knowledge queries | +| machinery *around* the agent | `hook_triggered` / `hook_completed` (with `trigger_event`) | LangGraph nodes, LlamaIndex workflow steps, CrewAI flow methods, guardrails | + +**A LangGraph node is a hook, not a nested agent**, and that is the one mapping +decision worth understanding, because it is the one you might be tempted to +"fix". `agent_id` is the primary facet across every session; it has to stay a +small, stable set of labels. Promote `retrieve`, `grade_documents` and +`should_continue` to agents and you drown the real agents in the filter, and the +session ends up labelled with whichever node happened to run first. Hook spans +draw identically on the timeline — same lanes, same durations — and you get a +per-node latency surface for free, with `hook_name` as its own facet. + +## LangChain / LangGraph + +Supported: `langchain-core >=1.4.7,<2`, `langgraph >=1.2,<2`. + +Attaches through LangChain's own callback configuration, so **every** callback +manager the framework builds carries it — including ones created inside chains +you never touch. + +| LangChain / LangGraph | Failproof AI | +|---|---| +| root run (the outermost chain/graph) | `agent_start` / `agent_end` | +| LangGraph node | `hook_triggered` / `hook_completed`, `trigger_event="graph_node"` | +| compiled subgraph | nested `agent_start` / `agent_end`, `agent_id="root/node"` | +| tool run | `tool_use` / `tool_result` | +| retriever run | `tool_use` / `tool_result`, output summarised | +| chat model / LLM run | `model_request` / `model_response`, paired on `request_id` | +| `interrupt()` | `human_wait` + `agent_pause` | +| `Command(resume=...)` | `agent_resume` + `human_input` | +| intermediate chains | **nothing**, unless you name them in `include_chains` | + +**Python 3.10 async nodes must forward `RunnableConfig`.** LangGraph cannot +automatically propagate callback context from an async node into a child +`ainvoke()` on Python 3.10. Without this, the child model or tool appears as a +separate root run: + +```python +from langchain_core.runnables import RunnableConfig + +async def plan(state, config: RunnableConfig): + reply = await model.ainvoke(state["messages"], config=config) + return {"messages": [reply]} +``` + +Python 3.11 and later propagate this context automatically. + +`session_id` resolution, in order — the first that produces a value wins: + +1. `instrument("langchain", session_id=...)`; +2. `config={"metadata": {"failproofai_sdk_session_id": sid}}` on the call — the + documented per-call key, and the one to use in a web service; +3. an enclosing `failproofai_sdk.session()` / `failproofai_sdk.agent()` scope; +4. `metadata["session_id" | "conversation_id" | "thread_id"]`; +5. the root run id. + +It is never synthesised from scratch. A made-up id would split one run into many +sessions, which is a silent wrong answer rather than a loud one. + +Three behaviours that surprise people: + +- **A `GraphInterrupt` is control flow, not an error.** LangGraph raises it + through the same error callback as a genuine failure, so a naive integration + paints every human approval red and ends the run as `failed`. The adapter + treats the whole interrupt family as control flow and emits the HITL pairs + instead. +- **Streaming never produces per-token events.** Time-to-first-token and the + chunk count are folded into the closing `model_response` as `fw_ttft_ms` and + `fw_chunks`. +- **`agent_id` is the graph or chain name**, never a run UUID. Framework ids land + in `fw_run_id` / `fw_node` / `fw_thread_id`. + +## CrewAI + +Supported: `crewai >=1.13,<2` (the release where the span tree and normalised +token usage are both present). Registers a listener on CrewAI's event bus; +`crew.kickoff()` is unchanged. + +| CrewAI | Failproof AI | +|---|---| +| crew kickoff | `agent_start` / `agent_end`, `agent_id` = crew name | +| agent execution | nested `agent_start` / `agent_end`, `agent_id` = the agent's **role** | +| task | no event of its own — folded into the agent execution that runs it | +| flow method | `hook_triggered` / `hook_completed`, `trigger_event="flow_method"` | +| guardrail | hook pair; a tripped guardrail closes with `outcome="rejected"` | +| tool usage | `tool_use` / `tool_result` | +| memory / knowledge query, save, retrieval | `tool_use` / `tool_result` (`memory.query`, `knowledge.search`, …) | +| LLM call | `model_request` / `model_response` | +| LLM stream chunks | folded into the closing `model_response` | + +`agent_id` is the crew name at the top and the **agent role** underneath — +`"researcher"`, `"editor"`. CrewAI's `agent.id` is a UUID and goes to +`fw_agent_id`, never to `agent_id`. CrewAI runs its own internal flow inside +every agent execution; that one is a pass-through and does not become a span. + +## LlamaIndex + +Supported: `llama-index-core >=0.14.23,<0.15`. Registers on the root dispatcher, +so one call covers every workflow and agent in the process. + +| LlamaIndex | Failproof AI | +|---|---| +| `Workflow.run` root span | session + `agent_start` / `agent_end` | +| nested `Workflow.run` span | nested `agent_start` / `agent_end` | +| workflow step | `hook_triggered` / `hook_completed`, `trigger_event="workflow_step"` | +| LLM chat start/end | `model_request` / `model_response`, paired on `request_id` | +| `FunctionTool.call` | `tool_use` / `tool_result` | +| retrieval start/end | `tool_use` / `tool_result`, output summarised | +| a tool that waits for an event | `human_wait` + `agent_pause`, then `agent_resume` + `human_input` | +| embeddings | nothing, unless `embeddings=True` | + +`agent_id` is the `FunctionAgent.name` when there is one and the workflow class +name otherwise — never a span id. + +**Token counts are best-effort here, and deliberately so.** LlamaIndex has no +standard usage field, so the raw usage dict always ships as `usage`, and the +top-level `input_tokens` / `output_tokens` are set **only** when a recognised key +is present. A model integration that names its counters something new gives you a +populated `usage` and blank token columns. That is the honest outcome; a +confident wrong number would be worse. + +**Known gap:** human-in-the-loop is captured only when the wait happens inside a +tool — the pattern LlamaIndex documents. A plain workflow step that waits for an +event is resolved by the runtime before anything observable happens, so there is +no signal to key a pause on. + +## Pydantic AI + +Supported: `pydantic-ai-slim >=2.0,<3`. Every tutorial written for v1 is wrong +for this range: `Agent(instrument=...)` was removed in 2.0. The adapter installs +itself as a capability instead — no OpenTelemetry SDK required, and it cannot +double-count against your own tracing. + +| Pydantic AI | Failproof AI | +|---|---| +| an agent run | `agent_start` / `agent_end` (plus `error` on failure) | +| a model request | `model_request` / `model_response` | +| a tool execution | `tool_use` / `tool_result` | +| graph nodes (`UserPromptNode`, `ModelRequestNode`, `CallToolsNode`) | **nothing** | + +Graph nodes are Pydantic AI's own loop machinery rather than steps you wrote — +unlike a LangGraph node — and everything they do that is worth seeing is already +covered by the model and tool spans. + +`session_id` is the run's `conversation_id`, so a conversation spanning several +runs is one session; an enclosing `failproofai_sdk.session()` / `failproofai_sdk.agent()` scope +always wins. `agent_id` is the `Agent`'s name. + +**One ordering rule, because it bites:** the capability is attached when an +`Agent` is constructed, so **agents built before `instrument()` are not +instrumented**, and agents built while instrumented keep the capability object +after `uninstrument()` (it turns into a pass-through and stops recording). +Construct your agents after `instrument()`, or attach it yourself: + +```python +from failproofai_sdk.integrations.pydantic_ai import FailproofAI + +agent = Agent("openai:gpt-5", capabilities=[FailproofAI()]) +``` + +## Mixing adapters with hand-written events + +They compose, and this is the supported way to add detail an adapter cannot know +about: + +```python +failproofai_sdk.instrument("langchain") + +with failproofai_sdk.agent("planner", goal=question): # your bracket + graph.invoke(...) # adapter events land here + with failproofai_sdk.tool_call("billing_check") as t: # your own tool span + t.output = check(user) +``` + +Adapter events join **that** session, with `parent_id="planner"`. An adapter +resolves identity from its own run id first, then that run's parent chain, then +whatever scope you have open — so a hand-written outer bracket and an adapter +produce one tree, not two. + +## What every adapter guarantees + +- **They never raise into your agent.** Every callback is wrapped: a failure logs + at WARNING with a traceback, and after three failures at the same call site + that site disables itself at ERROR. A broken adapter costs you a log line, not + your agent. Set `FAILPROOFAI_SDK_STRICT=1` to make it re-raise instead — do that in + tests and when debugging an adapter that records nothing. +- **Version ranges are checked at `instrument()` time.** Outside the supported + range: warn once, keep going. Named explicitly but not importable at all: + `ImportError` carrying the install command. `FAILPROOFAI_SDK_STRICT_INTEGRATIONS=1` + promotes every one of those warnings to an exception. +- **Framework-native ids are namespaced `fw_*`** — `fw_run_id`, `fw_node`, + `fw_task_id`, `fw_agent_id`, `fw_thread_id`. Flat, never nested. This is a + safety rule, not a style one: custom fields are merged last, so an un-namespaced + extra called `tool_name` or `duration_ms` would overwrite the real field. +- **Large payloads are truncated**, per field and per event, with a + `…[truncated]` marker. Prompts, retrieved documents and tool outputs are the + three largest strings in an agent process. +- **Every event carries `framework`, `framework_version` and + `integration_version`**, so you can tell adapter output from your own. +- **No per-token events, ever.** Streaming is folded into the closing + `model_response`. +- **Timestamps are stamped when the callback fires.** Nothing is backdated, so an + adapter cannot import a trace that already happened. + +## Verifying an adapter + +Everything in `../SKILL.md` §5 applies unchanged — the events go to the same +files. Four checks that are specific to adapter output: + +1. **The first event of each session is the root `agent_start`.** Anything + emitted before it can leave the session labelled by the wrong actor. +2. **`agent_id` values are names, not UUIDs** — `"planner"`, `"researcher"`, + the graph name. A UUID here means something is being passed through that + should have gone to `fw_agent_id`. +3. **`model_request` and `model_response` share a `request_id`**, and every + `model_response` carries an integer `duration_ms`. +4. **Nothing is left open.** Every `tool_use` has a `tool_result`, every + `agent_start` an `agent_end`. + +If an adapter records nothing at all, in this order: was the framework imported +**before** `instrument()` (auto-detect reads imported modules); did +`instrument()` return a non-empty tuple; and does the run produce anything with +`FAILPROOFAI_SDK_STRICT=1` set, which converts a swallowed adapter error into a raise. + +## A framework that is not on this list + +Use the shipped context managers at the framework's own boundaries — +`failproofai_sdk.agent()` around the run, `failproofai_sdk.tool_call()` in the tool hook. See +`integration.md`. Ask before writing a full callback adapter for an unsupported +framework: adding one to the SDK is usually the better answer. diff --git a/sdk/python/skill/references/integration.md b/sdk/python/skill/references/integration.md index 3ccc55031..bf87a685b 100644 --- a/sdk/python/skill/references/integration.md +++ b/sdk/python/skill/references/integration.md @@ -1,176 +1,152 @@ # Writing the integration -The SDK has no ambient session: every call needs `session_id` and `agent_id`, and -nothing carries them for you. Threading both through every function that might -emit an event is what makes instrumentation sprawl into a diff nobody wants to -review — and it is why integrations get abandoned halfway. +Identity is ambient. Bind it once per run with a context manager and every +`event.*` call inside — including calls in functions that have never heard of +Failproof AI — lands on the right session and agent. -Bind identity once per run, read it at the call sites. +You do not write the contextvar layer any more; it ships. This page is how to use +it, and what the two failure modes it exists to prevent look like when you route +around it. -## Don't use a module global +On a supported framework (LangChain/LangGraph, CrewAI, LlamaIndex, Pydantic AI), +read `frameworks.md` first — `failproofai_sdk.instrument()` does all of this for you. -The obvious shortcut breaks silently: +## The three scopes ```python -# WRONG -_current_session = None # or self.session_id on a shared client -``` - -The moment two runs overlap — two asyncio tasks, two threads, two requests in one -process — they share this variable. Events from both runs get whichever -`session_id` was written last. You do not get an error; you get **one session -containing two runs' events, interleaved**, and another session missing entirely. -Nothing about the output says the data is wrong. +import failproofai_sdk -Use `contextvars`. They are per-task and per-thread, and they are in the standard -library. +failproofai_sdk.configure(environment="production") -## The wrapper +with failproofai_sdk.agent("planner", goal=question): # agent_start / agent_end + with failproofai_sdk.tool_call("web_search", input={"q": question}) as t: + t.output = search(question) +``` -One module. Drop it in the agent's package, import it at the call sites. +| Scope | Emits | Yields | +|---|---|---| +| `failproofai_sdk.session(session_id=None, *, agent_id=None)` | **nothing** — identity only | the session id | +| `failproofai_sdk.agent(agent_id="main", *, session_id=None, goal=None, parent_id=AUTO, outcome="success", summary=None, **fields)` | `agent_start` on entry, `agent_end` on every exit | an `Identity` | +| `failproofai_sdk.tool_call(tool_name, *, tool_call_id=None, input=None, **fields)` | `tool_use` / `tool_result`, timed | a handle — set `.output`, read `.id` | -```python -# observability.py -import contextlib -import contextvars -import logging -import traceback -import uuid +Every one of them works as `with` **and** `async with`, with identical semantics +and byte-identical events. Nothing in a scope awaits, so the async form is the +same code; use whichever matches the function you are in. -import failproofai_sdk +Read the current identity anywhere with `failproofai_sdk.current()`, which returns a +frozen `Identity(session_id, agent_id, parent_id, depth)`. `session_id is None` +means nothing is bound. -log = logging.getLogger(__name__) +**`session()` versus `agent()`.** `session()` binds identity and emits nothing — +reach for it when the run is brackets you do not own (a request handler that +delegates to a framework), or when you want one session id to cover several +`agent()` blocks. `agent()` is what creates the session on the platform, because +a session is *defined* as something that emitted `agent_start`. -_session_id: contextvars.ContextVar[str] = contextvars.ContextVar("agenteye_session_id") -_agent_id: contextvars.ContextVar[str] = contextvars.ContextVar("agenteye_agent_id") +**Defaults, all of them chosen to be hard to get wrong:** -_FAILED = "failed" # NOT "failure" — see SKILL.md §3 -_SUCCESS = "success" +- `session_id=None` on `agent()`/`session()` inherits an already-bound session, + and generates a `uuid4().hex` only if there is none. A nested scope stays inside + one run rather than splitting it in two. +- `parent_id` defaults to the enclosing agent from the scope stack. Pass + `parent_id=None` to force a root span, or a string to override. +- `tool_call_id` defaults to `uuid4().hex` — unique process-wide, which is what + the correlation map needs (see `events.md`). +## What `agent()` does on the way out -def current_session_id() -> str | None: - """The session_id of the run in scope, or None. Needed by any sub-agent that - is its own function rather than an inline block.""" - return _session_id.get(None) +This is the exit path hand-written instrumentation always gets wrong, so it is +worth stating exactly: +| Exception | Events, in order | `outcome` | +|---|---|---| +| none | `agent_end` | `"success"` (or your `outcome=`) | +| any `Exception` | `error`, **then** `agent_end` | `"failed"` | +| `KeyboardInterrupt` / `SystemExit` | `error`, then `agent_end` | `"failed"` | +| `CancelledError` / `GeneratorExit` | `agent_end` only | `"cancelled"` | -def _emit(method: str, **fields) -> None: - """Emit an event with identity filled in from the current run. +The exception is always re-raised. `error` comes strictly *before* `agent_end`, +because the agent span closes at `agent_end` and anything after it is attributed +to nothing. A cancellation is not a failure and must not pollute the Errors +surface, so it emits no `error` event. - Unsupported payload leaves are stringified by the SDK writer. This - try/except covers mistakes made at the call site (a bad method name or a - reserved field). - """ - sid = _session_id.get(None) - if sid is None: - log.warning("failproofai_sdk: %s emitted outside a run() block — dropped", method) - return - try: - getattr(failproofai_sdk.event, method)( - session_id=sid, agent_id=_agent_id.get("main"), - **fields, - ) - except Exception: - log.exception("failproofai_sdk: failed to emit %s", method) - - -@contextlib.contextmanager -def run(*, session_id: str | None = None, agent_id: str = "main", - goal: str | None = None, parent_id: str | None = None): - """Bracket one run. Emits agent_start on entry, agent_end on every exit.""" - sid = session_id or uuid.uuid4().hex - t_sid = _session_id.set(sid) - t_aid = _agent_id.set(agent_id) - try: - failproofai_sdk.event.agent_start( - session_id=sid, agent_id=agent_id, goal=goal, parent_id=parent_id - ) - try: - yield sid - except BaseException as e: - _emit("error", error_type=type(e).__name__, message=str(e), - traceback=traceback.format_exc()) - failproofai_sdk.event.agent_end(session_id=sid, agent_id=agent_id, - outcome=_FAILED, summary=f"{type(e).__name__}: {e}") - raise - else: - failproofai_sdk.event.agent_end(session_id=sid, agent_id=agent_id, outcome=_SUCCESS) - finally: - _session_id.reset(t_sid) - _agent_id.reset(t_aid) - - -class _Box: - """Set .output inside a tool_call block to record the tool's result.""" - output = None - - -@contextlib.contextmanager -def tool_call(tool_name: str, *, tool_call_id: str | None = None, input=None): - """Bracket one tool call. Emits tool_use/tool_result and times it.""" - tcid = tool_call_id or uuid.uuid4().hex - _emit("tool_use", tool_name=tool_name, tool_call_id=tcid, input=input) - box = _Box() - try: - yield box - # BaseException, not Exception. `asyncio.CancelledError` inherits straight - # from BaseException on every supported Python, so `except Exception` does - # NOT see a cancelled tool — and a cancelled tool is a `tool_use` with no - # `tool_result` after it: an orphaned pending entry that also holds its - # correlation slot until the cap evicts it. Cancellation is the ordinary way - # an async tool ends when a timeout fires or a caller gives up, so this is - # the common path, not the exotic one. The same reasoning covers - # KeyboardInterrupt and SystemExit. - except BaseException as e: - _emit("tool_result", tool_name=tool_name, tool_call_id=tcid, - error=f"{type(e).__name__}: {e}") - raise - else: - _emit("tool_result", tool_name=tool_name, tool_call_id=tcid, output=box.output) +`tool_call()` on failure emits `tool_result(error="TypeName: message")` and **no +`error` event** — a tool failure the agent loop catches is not a run-level error, +and one that propagates is reported exactly once, by the enclosing `agent()`. +## Explicit identity still works -def model_request(**fields) -> None: - _emit("model_request", **fields) +`session_id` and `agent_id` are optional keyword arguments on all 15 `event.*` +methods, not removed ones. Passing them explicitly still works, and still wins +over whatever is bound: +```python +failproofai_sdk.event.tool_use(session_id=sid, agent_id="planner", + tool_name="web_search", tool_call_id="toolu_01") +``` -def model_response(**fields) -> None: - _emit("model_response", **fields) +Omit `session_id` with nothing bound and you get a **`TypeError`** naming both +fixes. That is deliberate: the alternative is dropping the event silently, and +this SDK already asks you to debug enough invisible failures. `agent_id` never +raises — it is a label, and it falls back to `"main"`. +## Don't use a module global -def submit(pool, fn, *args, **kwargs): - """pool.submit, but the worker inherits the current run. +The shortcut the scopes exist to replace, because it breaks silently: - Use this instead of pool.submit anywhere inside a run(). A bare submit gives - the worker an empty context, so every event it emits is dropped — see - "Threads will drop your events" below. Wrapping it here means a future call - site cannot forget. - """ - ctx = contextvars.copy_context() - return pool.submit(ctx.run, lambda: fn(*args, **kwargs)) +```python +# WRONG +_current_session = None # or self.session_id on a shared client ``` -`tool_call_id` must be unique **process-wide**, not just within a run — the SDK -keys its correlation map on the bare id, so two concurrent runs drawing from a -small space (`random.randint(1, 999)`, or a per-run `call_1` counter) will -occasionally pair the wrong two events and report a confident wrong duration. -`uuid4` or your framework's own id. See `events.md`. +The moment two runs overlap — two asyncio tasks, two threads, two requests in one +process — they share this variable. Events from both runs get whichever +`session_id` was written last. You do not get an error; you get **one session +containing two runs' events, interleaved**, and another session missing entirely. +Nothing about the output says the data is wrong. + +The scopes bind contextvars instead: per-task and per-thread, so overlapping runs +cannot see each other's identity. The agent stack they keep is immutable for the +same reason — a mutable stack shared by reference between tasks is the same bug +wearing a contextvars costume, and it passes every single-threaded test. -`run()` and `tool_call()` are sync context managers and work unchanged inside -`async def` — they wrap no I/O, so there is nothing to await. +## Threads will drop your events — `propagate()` is the fix -## Using it +This one is worth reading twice, because it is the failure everybody hits. + +`contextvars` propagate into asyncio tasks automatically — a task started inside +an `agent()` block inherits the session. **They do not propagate into new +threads.** A fresh thread starts with an empty context, so nothing is bound +there. ```python -import observability as obs +# WRONG — the worker has no session +pool.submit(dispatch, tool_call) +``` + +Wrap the callable: -with obs.run(agent_id="planner", goal=user_question) as session_id: - resp = call_model(messages) - with obs.tool_call("web_search", tool_call_id=tc.id, input=tc.input) as t: - t.output = search(tc.input["query"]) +```python +pool.submit(failproofai_sdk.propagate(dispatch), tool_call) +pool.map(failproofai_sdk.propagate(work), items) +threading.Thread(target=failproofai_sdk.propagate(work)).start() +loop.run_in_executor(None, failproofai_sdk.propagate(work), x) ``` -`agent_start`/`agent_end` and the error path come for free — including on an -exception, which is the exit path hand-written instrumentation always forgets. +`propagate(fn)` captures the identity bound *at the moment you call it* and binds +it inside the worker, then restores what was there. One name covers thread pools, +bare threads and `run_in_executor`. + +**Do not reach for `contextvars.copy_context().run` instead.** A `Context` object +cannot be entered by two threads at once — the second gets `RuntimeError: cannot +enter context` — so the copy-context form crashes the caller's worker on any +reuse, which is exactly what `pool.map` and a retried submit do. Mutations made +inside `ctx.run` also persist in that `Context`, so a reused one leaks the +previous call's agent stack into the next run. + +Since events now raise rather than vanish when nothing is bound, an un-propagated +worker announces itself with a `TypeError` naming `failproofai_sdk.propagate` instead of +producing a session that is quietly missing half its events. ## Where to put the calls @@ -179,100 +155,100 @@ is one edit site for all tools: ```python def dispatch(tool_call): - with obs.tool_call(tool_call.name, tool_call_id=tool_call.id, - input=tool_call.input) as t: + with failproofai_sdk.tool_call(tool_call.name, tool_call_id=tool_call.id, + input=tool_call.input) as t: t.output = TOOLS[tool_call.name](**tool_call.input) return t.output ``` Reuse the framework's own tool-call id — Anthropic and OpenAI both give you one. -It is already unique and it makes the events line up with your provider logs. +It is already unique, and it makes the events line up with your provider logs. -**One LLM wrapper.** Same idea on the model side: +**One LLM wrapper.** Same idea on the model side. Pass a `request_id` on both +halves so concurrent calls pair correctly, and set `duration_ms` yourself on the +response — it is not auto-computed for model events: ```python def call_model(messages, **kw): - obs.model_request(model=MODEL, messages=messages, tools=kw.get("tools")) - resp = client.messages.create(model=MODEL, messages=messages, **kw) - obs.model_response( - model=resp.model, - stop_reason=resp.stop_reason, - input_tokens=resp.usage.input_tokens, - output_tokens=resp.usage.output_tokens, - role=resp.role, - ) + rid = uuid.uuid4().hex + started = time.monotonic() + failproofai_sdk.event.model_request(model=MODEL, messages=messages, request_id=rid, + tools=kw.get("tools")) + try: + resp = client.messages.create(model=MODEL, messages=messages, **kw) + # BaseException, not Exception. `asyncio.CancelledError` inherits straight + # from BaseException on every supported Python, so `except Exception` does + # NOT see a cancelled tool — and a cancelled tool is a `tool_use` with no + # `tool_result` after it: an orphaned event that also holds its correlation + # slot until the cap evicts it. Cancellation is the ordinary way an async + # tool ends when a timeout fires or a caller gives up. + except BaseException as e: + failproofai_sdk.event.model_response( + model=MODEL, request_id=rid, error=f"{type(e).__name__}: {e}", + duration_ms=round((time.monotonic() - started) * 1000)) + raise + failproofai_sdk.event.model_response( + model=resp.model, request_id=rid, stop_reason=resp.stop_reason, + input_tokens=resp.usage.input_tokens, output_tokens=resp.usage.output_tokens, + role=resp.role, duration_ms=round((time.monotonic() - started) * 1000)) return resp ``` -**Sub-agents.** One session, several actors. Reuse the parent's `session_id`; -give each actor its own `agent_id`; set `parent_id` to the **parent's `agent_id`** -(not a session id — that silently nests nothing): +`duration_ms` must be a whole-millisecond **`int`** — a float is dropped on the +way in and the duration comes out empty. + +**Sub-agents.** One session, several actors. Nest the scopes and the wiring is +automatic — the inner `agent()` inherits the session and takes the outer agent as +its `parent_id`: ```python async def researcher(topic): - with obs.run(session_id=obs.current_session_id(), # NOT a new session - agent_id="researcher", parent_id="planner"): + async with failproofai_sdk.agent("researcher", goal=topic): ... + +async with failproofai_sdk.agent("planner", goal=question): + await researcher(topic) # session inherited, parent_id="planner" ``` **Yes, this means several `agent_start` events on one `session_id` — that is correct and intended.** `SKILL.md` §2 says "emit `agent_start` at the top of the run"; read that as *once per agent*, not once per session. One `agent_start` per -actor is what makes sub-agents appear as distinct, nested spans; a session with -one `agent_start` is simply a session with one actor. The session still exists -from the first one. - -Use `current_session_id()` rather than threading `session_id` through as a -parameter — a sub-agent is usually its own function, and the whole point of the -contextvar is that you don't have to. - -## Threads will drop your events +actor is what makes sub-agents appear as distinct, nested spans. -This one is worth reading twice, because it fails the quiet way. - -`contextvars` propagate to asyncio tasks automatically — a task started inside a -`run()` block inherits the session. **They do not propagate to new threads.** A -thread starts with an empty context, so `_session_id.get(None)` returns `None` -there, and every event emitted from that thread is dropped. - -So if the agent dispatches tools to a `ThreadPoolExecutor`, the naive version -records nothing from the workers: - -```python -# WRONG — worker sees no session -pool.submit(dispatch, tool_call) -``` +If a sub-agent runs in a different function that is called *outside* the parent's +block, pass `session_id=failproofai_sdk.current().session_id` explicitly rather than +letting it generate a new one. -Copy the context in: +**A service.** For an HTTP handler or a queue worker you usually already have a +per-run id — request id, job id, trace id. **Use it as the `session_id`** rather +than generating one: then a session in Failproof AI and a request in your own logs +are the same string, and cross-referencing an incident stops being detective +work. ```python -import contextvars - -ctx = contextvars.copy_context() -pool.submit(ctx.run, dispatch, tool_call) +@app.post("/chat") +async def chat(req: Request, body: ChatBody): + async with failproofai_sdk.agent("assistant", session_id=req.headers["x-request-id"], + goal=body.message): + return await run_agent(body.message) ``` -The wrapper above logs a warning when this happens, which is the only reason you -will notice. If you see `emitted outside a run() block` in your logs, this is -almost always why. - ## Frameworks -Nothing here is framework-specific — the wrapper is 60 lines of stdlib. Where a -framework gives you a callback surface, put `run()` at its outermost boundary and -`tool_call()` in its tool hook. - -| Framework | Session bracket | Tool pair | -|---|---|---| -| plain loop | around the loop function | in the dispatcher | -| LangChain / LangGraph | around `.invoke()` / `.astream()`, or a callback handler's `on_chain_start` / `on_chain_end` | `on_tool_start` / `on_tool_end` | -| an HTTP agent service | request middleware, keyed on the request id | in the dispatcher | -| a queue worker | around the job handler, keyed on the job id | in the dispatcher | - -For an HTTP service or a worker, you usually already have a per-run id (request -id, job id, trace id). **Use it as `session_id`** rather than generating a new -one — then a session in AgentEye and a request in your own logs are the same -string, and cross-referencing an incident stops being detective work. +| Framework | What to do | +|---|---| +| LangChain / LangGraph | `failproofai_sdk.instrument()` — see `frameworks.md` | +| CrewAI | `failproofai_sdk.instrument()` | +| LlamaIndex | `failproofai_sdk.instrument()` | +| Pydantic AI | `failproofai_sdk.instrument()` | +| a plain agent loop | `agent()` around the loop, `tool_call()` in the dispatcher | +| an HTTP agent service | `agent()` in request middleware, keyed on the request id | +| a queue worker | `agent()` around the job handler, keyed on the job id | +| anything else with a callback surface | `agent()` at its outermost boundary, `tool_call()` in its tool hook | + +Adapters and hand-written scopes compose: instrument the framework, wrap the call +in your own `failproofai_sdk.agent(...)`, and the adapter's events join **that** session +with your agent as their parent. `frameworks.md` has the details. ## Testing your integration @@ -300,17 +276,17 @@ def events(tmp_path, monkeypatch): ```python def test_run_emits_a_session(events): - import failproofai_sdk, observability as obs - with obs.run(agent_id="planner", goal="test"): + import failproofai_sdk + with failproofai_sdk.agent("planner", goal="test"): pass - failproofai_sdk._writer.flush_now() # don't wait 0.5s for the flush thread + failproofai_sdk._writer.flush_now() # don't wait for the flush thread types = [e["type"] for e in events()] assert types == ["agent_start", "agent_end"] def test_failure_is_recorded_as_failed(events): - import failproofai_sdk, observability as obs, pytest + import failproofai_sdk, pytest with pytest.raises(ValueError): - with obs.run(agent_id="planner"): + with failproofai_sdk.agent("planner"): raise ValueError("boom") failproofai_sdk._writer.flush_now() ev = {e["type"]: e for e in events()} @@ -323,17 +299,44 @@ test finishes before the flush cycle and reads an empty directory — a flaky-looking failure with a real cause. Note the leading underscore: `_writer` is not a public API, so pin your SDK version if your tests depend on it. -Assert on `type`, `session_id`, and `outcome`. Those are the three that break +Assert on `type`, `session_id` and `outcome`. Those are the three that break silently in production. +**Test the overlap, not just the happy path.** A single run passes even when +identity is a module global; mixing only shows up once two runs overlap, which is +production and not your laptop: + +```python +import asyncio + +def test_two_runs_do_not_mix(events): + import failproofai_sdk + + async def run(name): + async with failproofai_sdk.agent(name, session_id=name): + await asyncio.sleep(0) # force the interleave + failproofai_sdk.event.tool_use(tool_name="t", tool_call_id=f"{name}-1") + + async def both(): + await asyncio.gather(run("a"), run("b")) + + asyncio.run(both()) + failproofai_sdk._writer.flush_now() + for e in events(): + assert e["agent_id"] == e["session_id"] # no event crossed over +``` + +Without the `await asyncio.sleep(0)` the tasks do not interleave and the test +passes against a broken implementation, which makes it worthless. + **Add one test with an awkward payload**, because this is the failure that costs you everything and it is invisible in normal testing: ```python def test_unserializable_payload_does_not_kill_the_writer(events): - import failproofai_sdk, datetime, observability as obs - with obs.run(agent_id="planner"): - with obs.tool_call("clock") as t: + import failproofai_sdk, datetime + with failproofai_sdk.agent("planner"): + with failproofai_sdk.tool_call("clock") as t: t.output = {"at": datetime.datetime.now()} # a real tool does this failproofai_sdk._writer.flush_now() assert [e["type"] for e in events()] == [ diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index d914a82b1..65d10438c 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -1,31 +1,51 @@ -"""Nothing in this suite may write to the developer's real event spool. - -This is not hypothetical. Two tests here build an `EventWriter` with a long -flush interval to inspect its queue, and never flush it — which is the point of -those tests. But every writer registers itself in `_writer._live_writers`, and -`_flush_all_at_exit` flushes ALL of them at interpreter exit. That runs long -after pytest has torn down its fixtures, so whatever redirection a test applied -is already undone and `get_base_dir()` resolves to the real `~/.agenteye` -again. One run of the queue-cap tests deposited over 160,000 synthetic events -into a live spool, where the collector would have shipped them to a real -dashboard as though an agent had emitted them. - -A per-test fixture cannot fix that, because the write happens after the last -fixture has gone. So the redirection is applied HERE, at import, straight into -`os.environ` rather than through `monkeypatch` — pytest undoes monkeypatch at -session end, and session end is still earlier than the flush. - -`setdefault` rather than assignment, so a developer who exports `AGENTEYE_HOME` -to point at a scratch spool of their own keeps it. - -`test_resolver_umbrella.py` deletes this variable per test through monkeypatch, -so the resolution rules themselves are still tested against a clean environment. +"""Suite-wide isolation. + +This SDK is built on module-level singletons, so state leaks between tests in +four ways, and every one of them has bitten this package. + +1. **At interpreter exit.** Two tests build an `EventWriter` with a long flush + interval to inspect its queue and never flush it — the point of those tests. + But every writer registers in `_writer._live_writers`, and + `_flush_all_at_exit` flushes ALL of them when the process ends, long after + pytest has torn its fixtures down. Whatever redirection a test applied is + already undone by then, so `get_base_dir()` resolves to the real spool again. + One run of the queue-cap tests deposited over 160,000 synthetic events into a + live `~/.agenteye`, where a configured collector would have shipped them to a + real dashboard as though an agent had emitted them. + + A fixture cannot fix that, because the write happens after the last fixture + is gone. So the redirection is applied at IMPORT, straight into `os.environ` + rather than through `monkeypatch` — pytest undoes monkeypatch at session end, + and session end is still earlier than the flush. + +2. **`_resolver._base_dir` during a test.** Same destination, different clock. + Every test gets `tmp_path` whether it asked for one or not. + +3. **`_environment` and the writer's flush interval.** `configure()` mutates + process-global state and nothing puts it back. + +4. **The identity contextvars.** A leaked `session_id` in production means + events attributed to the wrong run, accepted at HTTP 200 — the exact silent + failure that layer exists to prevent. So the leak is asserted and the leaking + test *fails*. It is repaired afterwards only so the failure stays readable as + one test rather than cascading through the rest of the run. + +`test_sdk.py` has its own in-file autouse `_reset_environment`; these are +additive and idempotent with it. """ import atexit import os import shutil import tempfile +import pytest + +# ── (1) survives to interpreter exit ───────────────────────────────────────── +# +# `setdefault`, so a developer who exports `AGENTEYE_HOME` to point at a scratch +# spool of their own keeps it. `test_resolver_umbrella.py` deletes this variable +# per test through monkeypatch, so the resolution rules themselves are still +# tested against a clean environment. _SANDBOX = tempfile.mkdtemp(prefix="failproofai-sdk-tests-") os.environ.setdefault("AGENTEYE_HOME", _SANDBOX) @@ -33,3 +53,72 @@ # atexit's LIFO order and therefore runs LAST — after the final flush has written # into the sandbox we are about to remove. atexit.register(shutil.rmtree, _SANDBOX, True) + +import failproofai_sdk._environment as _environment # noqa: E402 +import failproofai_sdk._resolver as _resolver # noqa: E402 +from failproofai_sdk import _context, _runtime # noqa: E402 +from failproofai_sdk._events import EventNamespace # noqa: E402 + + +class RecordingWriter: + """A writer that keeps entries in memory. No disk, no flush timing.""" + + def __init__(self) -> None: + self.entries: list[dict] = [] + + def submit(self, entry: dict) -> None: + self.entries.append(entry) + + def last(self) -> dict: + return self.entries[-1] + + def types(self) -> list[str]: + return [e["type"] for e in self.entries] + + +@pytest.fixture(autouse=True) +def _sdk_global_state(tmp_path, monkeypatch): + """(2) and (3): a private spool per test, and process globals restored.""" + monkeypatch.delenv("AGENTEYE_HOME", raising=False) + monkeypatch.delenv("AGENTEYE_ENVIRONMENT", raising=False) + + base_dir = _resolver._base_dir + environment = _environment._environment + flush_interval = _runtime.writer._flush_interval + + _resolver.set_base_dir(tmp_path) + try: + yield + finally: + _resolver.set_base_dir(base_dir) + _environment._environment = environment + _runtime.writer._flush_interval = flush_interval + + +@pytest.fixture(autouse=True) +def _no_context_leak(): + """(4): an unbalanced scope fails the test that left it open.""" + yield + session_id, stack = _context.snapshot() + if session_id is not None or stack: + # Repair before failing: without this every subsequent test in the run + # also fails and the real culprit is impossible to spot. + _context.restore((None, ())) + pytest.fail( + "failproofai_sdk identity contextvars leaked out of this test: " + f"session_id={session_id!r}, agent_stack={stack!r}. " + "A scope was entered without being exited." + ) + + +@pytest.fixture() +def events(monkeypatch): + """Swap the process-wide namespace for a recording one. + + `_scopes` resolves `_runtime.event` at call time precisely so this works. + Tests must not emit through the real writer: `test_sdk.py` asserts on the + exact contents of the events directory. + """ + writer = RecordingWriter() + monkeypatch.setattr(_runtime, "event", EventNamespace(writer)) + return writer diff --git a/sdk/python/tests/integrations/__init__.py b/sdk/python/tests/integrations/__init__.py new file mode 100644 index 000000000..4cd38a9a1 --- /dev/null +++ b/sdk/python/tests/integrations/__init__.py @@ -0,0 +1,6 @@ +"""Per-framework adapter tests. + +A package (not a bare directory) because `tests/` is one: without an +`__init__.py` two files called `test_.py` in different directories +would collide on the module name under rootdir-based collection. +""" diff --git a/sdk/python/tests/integrations/test_crewai.py b/sdk/python/tests/integrations/test_crewai.py new file mode 100644 index 000000000..97f30fba6 --- /dev/null +++ b/sdk/python/tests/integrations/test_crewai.py @@ -0,0 +1,1299 @@ +"""The CrewAI adapter, against a real Crew and a fake model. + +Every structural test here drives a genuine `crewai.Crew` and then asserts on +**the JSONL the writer actually wrote**. No mocks: a mock-based adapter test +proves the adapter calls the functions the test says it calls, which was never +in doubt. + +Two tests earn their place above the rest: + +* `TestAntiDrift` — every other test in this file would still pass if CrewAI + renamed `setup_listeners`, dropped `started_event_id`, or changed the + predicate that decides whether a handler is async. Our handlers would simply + never be called, the crew would run fine, and we would silently record + nothing. +* `test_handlers_are_all_async` — this is not a style assertion. Measured on + crewai 1.15.8, running the crew in this file with **sync** handlers produced a + wrong event stream in 13 of 25 runs: `emit()` dispatches sync handlers onto a + ten-worker pool, so a `tool_usage_finished` can be handled before its + `tool_usage_started` and the `tool_result` is then **dropped entirely**, not + merely reordered. The same 25 runs with async handlers were correct 25 times. +""" + +import ast +import dataclasses +import inspect +import json +import os +import re +import shutil +import uuid + +import pytest + +import failproofai_sdk +from failproofai_sdk import _runtime, _schema +from failproofai_sdk.integrations import _core + +pytestmark = pytest.mark.framework + +_REQUIRE_FRAMEWORKS = os.environ.get("AGENTEYE_TESTS_REQUIRE_FRAMEWORKS", "").strip().lower() in { + "1", + "true", + "yes", + "on", +} + +# Read at crewai import time, so they have to be set before the import below. +# Without them the first run opens a network client and writes a preference file. +os.environ.setdefault("CREWAI_DISABLE_TELEMETRY", "true") +os.environ.setdefault("OTEL_SDK_DISABLED", "true") +os.environ.setdefault("CREWAI_TRACING_ENABLED", "false") + +try: + from crewai import Agent, Crew, Task + from crewai.events import crewai_event_bus + from crewai.events.base_event_listener import BaseEventListener + from crewai.events.event_context import restore_event_scope + from crewai.events.types.crew_events import CrewKickoffStartedEvent + from crewai.events.types.tool_usage_events import ToolUsageStartedEvent + from crewai.events.utils.handlers import _get_param_count, is_async_handler + from crewai.llms.base_llm import BaseLLM, LLMCallType, llm_call_context + from crewai.tools import BaseTool +except ImportError: # pragma: no cover - exercised only on a bare environment + # `pytest.importorskip` is fail-open: misspell the module and every test in + # the file skips while CI stays green having tested nothing. The framework + # CI leg sets AGENTEYE_TESTS_REQUIRE_FRAMEWORKS=1 to turn that into a hard + # failure. + if _REQUIRE_FRAMEWORKS: + raise + pytest.skip("crewai is not installed", allow_module_level=True) + +from failproofai_sdk.integrations import crewai as adapter_module # noqa: E402 +from failproofai_sdk.integrations.crewai import ( # noqa: E402 + STORE_TOOLS, + TABLE, + FailproofAICrewListener, + _tokens, + adapter, +) + +UUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-", re.ASCII) + + +# --------------------------------------------------------------------------- +# A fake model, a fake tool, and a real crew +# --------------------------------------------------------------------------- + +class ScriptedLLM(BaseLLM): + """Replays canned ReAct turns through CrewAI's own event helpers. + + Subclassing `BaseLLM` rather than patching `litellm` matters: the events the + adapter sees are then the framework's real ones, emitted from the real call + site with a real `call_id` scope, not something this file invented. + """ + + responses: list = [] + stream_pieces: int = 0 + raise_at: int = -1 + _index: int = 0 + + def call( + self, + messages, + tools=None, + callbacks=None, + available_functions=None, + from_task=None, + from_agent=None, + response_model=None, + ): + with llm_call_context(): + self._emit_call_started_event( + messages=messages, tools=tools, from_task=from_task, from_agent=from_agent + ) + index = self._index + self._index = index + 1 + if index == self.raise_at: + self._emit_call_failed_event( + error="provider exploded", from_task=from_task, from_agent=from_agent + ) + raise RuntimeError("provider exploded") + reply = self.responses[min(index, len(self.responses) - 1)] + for piece in range(self.stream_pieces): + self._emit_stream_chunk_event( + chunk=f"c{piece}", + from_task=from_task, + from_agent=from_agent, + call_type=LLMCallType.LLM_CALL, + ) + self._emit_call_completed_event( + response=reply, + call_type=LLMCallType.LLM_CALL, + from_task=from_task, + from_agent=from_agent, + messages=messages, + usage={"prompt_tokens": 42, "completion_tokens": 13}, + finish_reason="stop", + ) + return reply + + def supports_function_calling(self) -> bool: + return False + + def supports_stop_words(self) -> bool: + return True + + def get_context_window_size(self) -> int: + return 8192 + + +class Adder(BaseTool): + name: str = "adder" + description: str = 'Adds two integers. Input: {"a": , "b": }' + + def _run(self, a: int = 0, b: int = 0) -> str: + return str(int(a) + int(b)) + + +class Exploder(BaseTool): + name: str = "exploder" + description: str = "Always raises." + + def _run(self, **kwargs) -> str: + raise ValueError("tool blew up") + + +USE_TOOL = 'Thought: I should add.\nAction: adder\nAction Input: {"a": 2, "b": 3}' +USE_EXPLODER = "Thought: I should try.\nAction: exploder\nAction Input: {}" +FINAL = "Thought: done.\nFinal Answer: 5" + + +def build_crew(llm, *, tools=None, guardrail=None, guardrail_max_retries=1): + analyst = Agent( + role="Arithmetic Analyst", + goal="Answer arithmetic questions exactly", + backstory="A careful analyst.", + llm=llm, + tools=tools or [], + verbose=False, + ) + task = Task( + description="What is 2 + 3?", + expected_output="The number.", + agent=analyst, + name="add-two-numbers", + guardrail=guardrail, + guardrail_max_retries=guardrail_max_retries, + ) + return Crew(agents=[analyst], tasks=[task], name="Arithmetic Crew", verbose=False) + + +def tool_crew(**kwargs): + llm = ScriptedLLM(model="scripted/fake-1", responses=[USE_TOOL, FINAL], **kwargs) + return build_crew(llm, tools=[Adder()]) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture(autouse=True) +def _no_network(monkeypatch): + """The layer that survives someone adding a test without reading this file.""" + import socket + + def _blocked(*args, **kwargs): + raise AssertionError("a test tried to open a network connection") + + monkeypatch.setattr(socket.socket, "connect", _blocked) + + +@pytest.fixture(autouse=True) +def _clean_bus_scope(): + """CrewAI's scope stack is a contextvar with a hard depth cap of 100. + + A test that emits a `*_started` event by hand and never emits its ending + event leaves an entry on it forever; a hundred of those and every later + test in the process dies inside `push_event_scope`. + """ + yield + restore_event_scope(()) + + +@pytest.fixture() +def emitted(tmp_path): + """Read back the real JSONL the writer produced during this test. + + The flush interval goes to an hour because event filenames only carry + millisecond resolution: two flushes inside the same millisecond write to the + same path and the second clobbers the first. The background thread is parked + and every flush here is explicit. + """ + _runtime.writer.set_flush_interval(3600) + failproofai_sdk._writer.flush_now() + events_dir = tmp_path / "events" + if events_dir.exists(): + shutil.rmtree(events_dir) + + def read(): + crewai_event_bus.flush(timeout=30) + failproofai_sdk._writer.flush_now() + if not events_dir.exists(): + return [] + return [ + json.loads(line) + for path in sorted(events_dir.glob("*.jsonl")) + for line in path.read_text().splitlines() + if line.strip() + ] + + return read + + +@pytest.fixture() +def instrumented(): + """`instrument("crewai")` for the duration of one test, then put it back.""" + failproofai_sdk.instrument("crewai") + try: + yield adapter + finally: + failproofai_sdk.uninstrument("crewai") + + +def kickoff(crew): + """`kickoff()` flushes the bus *before* emitting crew_kickoff_completed, so + the final event is still in flight when it returns.""" + try: + return crew.kickoff() + finally: + crewai_event_bus.flush(timeout=30) + + +def types_of(events): + return [event["type"] for event in events] + + +# --------------------------------------------------------------------------- +# The representative run +# --------------------------------------------------------------------------- + +class TestRepresentativeRun: + def test_exact_event_type_sequence(self, instrumented, emitted): + result = kickoff(tool_crew()) + events = emitted() + assert result.raw == "5" + assert types_of(events) == [ + "agent_start", # the crew + "agent_start", # the agent that runs the task + "model_request", + "model_response", + "tool_use", + "tool_result", + "model_request", + "model_response", + "agent_end", # the agent + "agent_end", # the crew + ] + + def test_root_agent_start_is_the_sessions_first_event(self, instrumented, emitted): + kickoff(tool_crew()) + events = emitted() + # `agent_sessions.agent_id = any(...)` over an ORDER BY (session_id, ts) + # table returns the FIRST agent_id by time, so the sessions list shows + # whatever came first. It has to be the crew. + assert events[0]["type"] == "agent_start" + assert events[0]["agent_id"] == "Arithmetic Crew" + assert events[0].get("parent_id") is None + assert len({event["session_id"] for event in events}) == 1 + + def test_timestamps_are_monotonic(self, instrumented, emitted): + kickoff(tool_crew()) + stamps = [event["timestamp"] for event in emitted()] + assert stamps == sorted(stamps) + + def test_the_session_is_closed(self, instrumented, emitted): + events = emitted() if kickoff(tool_crew()) else [] + starts = [e for e in events if e["type"] == "agent_start"] + ends = [e for e in events if e["type"] == "agent_end"] + assert len(starts) == len(ends) == 2 + assert [e["outcome"] for e in ends] == ["success", "success"] + # Every leaf closed too: an open tool_use leaves the session `ongoing` + # forever, because agent_end force-closes pauses but not tools. + assert types_of(events).count("tool_use") == types_of(events).count("tool_result") + assert types_of(events).count("model_request") == types_of(events).count( + "model_response" + ) + + def test_agent_ids_are_names_never_uuids(self, instrumented, emitted): + events = emitted() if kickoff(tool_crew()) else [] + agent_ids = {event["agent_id"] for event in events} + assert agent_ids == {"Arithmetic Crew", "Arithmetic Analyst"} + assert not any(UUID_RE.match(value) for value in agent_ids) + # CrewAI's own agent id IS a uuid, and it goes to fw_agent_id. + nested = next(e for e in events if e["type"] == "agent_start" and e.get("parent_id")) + assert nested["parent_id"] == "Arithmetic Crew" + uuid.UUID(nested["fw_agent_id"]) + + def test_every_event_carries_the_framework_triple(self, instrumented, emitted): + events = emitted() if kickoff(tool_crew()) else [] + assert events + for event in events: + assert event["framework"] == "crewai" + assert event["framework_version"] + assert event["integration_version"] == failproofai_sdk.__version__ + + def test_every_leaf_hangs_off_an_open_agent(self, instrumented, emitted): + """Invariant 1: a leaf whose agent_id has no open agent_start makes the + dashboard synthesize a never-ending root span.""" + open_agents = set() + for event in emitted() if kickoff(tool_crew()) else []: + if event["type"] == "agent_start": + open_agents.add(event["agent_id"]) + elif event["type"] == "agent_end": + open_agents.discard(event["agent_id"]) + else: + assert event["agent_id"] in open_agents, event + + +# --------------------------------------------------------------------------- +# Correlation +# --------------------------------------------------------------------------- + +class TestCorrelation: + def test_model_events_pair_on_request_id_and_carry_int_durations( + self, instrumented, emitted + ): + events = emitted() if kickoff(tool_crew()) else [] + requests = [e for e in events if e["type"] == "model_request"] + responses = [e for e in events if e["type"] == "model_response"] + assert len(requests) == len(responses) == 2 + assert [e["request_id"] for e in requests] == [e["request_id"] for e in responses] + assert len({e["request_id"] for e in requests}) == 2 + for response in responses: + # `durationOf` prefers the closing event's duration_ms over + # end-start, which is what keeps model durations correct even when + # the dashboard's FIFO pairing brackets the wrong pair. It must be an + # int: the server's JSON parser drops floats and NULLs the column. + assert isinstance(response["duration_ms"], int) + assert not isinstance(response["duration_ms"], bool) + + def test_model_response_carries_normalized_and_raw_usage(self, instrumented, emitted): + events = emitted() if kickoff(tool_crew()) else [] + response = next(e for e in events if e["type"] == "model_response") + assert response["input_tokens"] == 42 + assert response["output_tokens"] == 13 + assert response["usage"] == { + "input_tokens": 42, + "output_tokens": 13, + "total_tokens": 55, + } + assert response["fw_usage_raw"] == {"prompt_tokens": 42, "completion_tokens": 13} + + def test_tool_events_pair_on_tool_call_id_with_a_duration(self, instrumented, emitted): + events = emitted() if kickoff(tool_crew()) else [] + use = next(e for e in events if e["type"] == "tool_use") + result = next(e for e in events if e["type"] == "tool_result") + assert use["tool_call_id"] == result["tool_call_id"] + assert use["tool_name"] == result["tool_name"] == "adder" + assert use["input"] == {"a": 2, "b": 3} + assert result["output"] == "5" + assert isinstance(result["duration_ms"], int) + # The framework's own uuid4, verbatim — so our rows line up with theirs. + uuid.UUID(use["tool_call_id"]) + + @pytest.mark.parametrize( + ("usage", "expected"), + [ + ({"prompt_tokens": 5, "completion_tokens": 6}, (5, 6, 11)), + ({"input_tokens": 5, "output_tokens": 6}, (5, 6, 11)), + ({"inputTokens": 5, "outputTokens": 6, "totalTokens": 99}, (5, 6, 99)), + ({"prompt_tokens": 5.0, "completion_tokens": "6"}, (5, 6, 11)), + ], + ) + def test_token_counts_read_both_provider_spellings(self, usage, expected): + """Reading only one spelling reports zero tokens for half the providers, + at HTTP 200, forever.""" + input_tokens, output_tokens, normalized = _tokens(usage) + assert (input_tokens, output_tokens, normalized["total_tokens"]) == expected + + @pytest.mark.parametrize("usage", [None, "not a dict", {}, {"prompt_tokens": True}]) + def test_token_counts_survive_a_useless_usage_dict(self, usage): + assert _tokens(usage)[:2] == (None, None) + + +# --------------------------------------------------------------------------- +# Payload discipline +# --------------------------------------------------------------------------- + +def _declared_field_names(): + names = {"timestamp", "session_id", "agent_id", "type", "environment"} + for obj in vars(_schema).values(): + if dataclasses.is_dataclass(obj) and isinstance(obj, type): + names.update(field.name for field in dataclasses.fields(obj)) + names.discard("extra_fields") + return names + + +class TestPayloadDiscipline: + def test_no_event_carries_an_unnamespaced_framework_field(self, instrumented, emitted): + """`_schema._build()` merges extras LAST, so an extra called `tool_name`, + `model` or `outcome` silently overwrites the declared field and changes + the promoted column. Everything framework-specific must be + `fw_*`.""" + allowed = _declared_field_names() | _core.ALLOWED_TOP_LEVEL + events = emitted() if kickoff(tool_crew()) else [] + assert events + for event in events: + for key in event: + assert key.startswith("fw_") or key in allowed, (key, event["type"]) + + def test_the_namespaced_fields_survived_the_guard(self, instrumented, emitted): + events = emitted() if kickoff(tool_crew()) else [] + keys = {key for event in events for key in event if key.startswith("fw_")} + assert {"fw_kind", "fw_agent_id", "fw_task_name", "fw_call_id"} <= keys + assert not (keys & _core.FORBIDDEN_EXTRAS) + + def test_no_per_token_events(self, instrumented, emitted): + """A 500-token response must not be 500 stored rows.""" + crew = build_crew( + ScriptedLLM(model="scripted/fake-1", responses=[FINAL], stream_pieces=7) + ) + kickoff(crew) + events = emitted() + assert types_of(events) == [ + "agent_start", + "agent_start", + "model_request", + "model_response", + "agent_end", + "agent_end", + ] + response = next(e for e in events if e["type"] == "model_response") + assert response["fw_streamed"] is True + assert response["fw_chunks"] == 7 + assert isinstance(response["fw_ttft_ms"], int) + + +# --------------------------------------------------------------------------- +# Failure paths +# --------------------------------------------------------------------------- + +class TestFailures: + def test_a_failed_crew_ends_failed_and_is_not_double_counted( + self, instrumented, emitted + ): + def never_ok(output): + return (False, "never good enough") + + crew = build_crew( + ScriptedLLM(model="scripted/fake-1", responses=[FINAL]), + guardrail=never_ok, + guardrail_max_retries=1, + ) + with pytest.raises(Exception, match="guardrail"): + kickoff(crew) + events = emitted() + + # `sessionSummary.errorCount` counts standalone `error` events AND + # failure outcomes, so emitting both for one failure double-counts it. + assert "error" not in types_of(events) + failed = [e for e in events if e["type"] == "agent_end" and e["outcome"] == "failed"] + assert len(failed) == 1 + assert failed[0]["agent_id"] == "Arithmetic Crew" + assert "guardrail" in failed[0]["summary"] + # "failed", never "failure": the server only counts + # error|failed|timeout|rejected. + assert {e["outcome"] for e in events if e["type"] == "agent_end"} == { + "success", + "failed", + } + # and the session is still closed + assert types_of(events).count("agent_start") == types_of(events).count("agent_end") + + def test_a_tripped_guardrail_is_a_rejected_hook(self, instrumented, emitted): + def never_ok(output): + return (False, "never good enough") + + crew = build_crew( + ScriptedLLM(model="scripted/fake-1", responses=[FINAL]), + guardrail=never_ok, + guardrail_max_retries=1, + ) + with pytest.raises(Exception, match="guardrail"): + kickoff(crew) + events = emitted() + hooks = [e for e in events if e["type"] == "hook_completed"] + assert hooks + for hook in hooks: + # "rejected" is in the server's failure vocabulary, so it paints red + # instead of reading as a hook that succeeded at saying no. + assert hook["outcome"] == "rejected" + assert hook["error"] == "never good enough" + assert isinstance(hook["duration_ms"], int) + triggered = [e for e in events if e["type"] == "hook_triggered"] + assert len(triggered) == len(hooks) + assert {e["trigger_event"] for e in triggered} == {"guardrail"} + + def test_a_tool_error_is_reported_on_the_tool_result_only(self, instrumented, emitted): + llm = ScriptedLLM(model="scripted/fake-1", responses=[USE_EXPLODER, FINAL]) + kickoff(build_crew(llm, tools=[Exploder()])) + events = emitted() + assert "error" not in types_of(events) + results = [e for e in events if e["type"] == "tool_result"] + assert results + for result in results: + assert "tool blew up" in result["error"] + # A tool failure the agent loop catches and retries is not a run-level + # failure, so the crew still ends successfully. + assert [e["outcome"] for e in events if e["type"] == "agent_end"] == [ + "success", + "success", + ] + + def test_a_failed_model_call_is_reported_on_the_model_response( + self, instrumented, emitted + ): + llm = ScriptedLLM(model="scripted/fake-1", responses=[FINAL], raise_at=0) + kickoff(build_crew(llm)) + events = emitted() + assert "error" not in types_of(events) + failed = [e for e in events if e["type"] == "model_response" and e.get("error")] + assert len(failed) == 1 + assert "exploded" in failed[0]["error"] + assert isinstance(failed[0]["duration_ms"], int) + assert failed[0]["request_id"] + # every request still has exactly one response + assert types_of(events).count("model_request") == types_of(events).count( + "model_response" + ) + + +# --------------------------------------------------------------------------- +# Never break the host +# --------------------------------------------------------------------------- + +class TestNeverBreaksTheHost: + def test_a_translator_that_raises_on_every_call_costs_nothing( + self, instrumented, emitted, caplog + ): + class Boom: + def __getattr__(self, name): + raise RuntimeError(f"translator exploded on {name}") + + instrumented._tracker = Boom() + result = kickoff(tool_crew()) + assert result.raw == "5" + assert emitted() == [] + assert any("failed" in record.message for record in caplog.records) + + def test_an_adapter_with_no_tracker_at_all_costs_nothing(self, instrumented): + instrumented._tracker = None + assert kickoff(tool_crew()).raw == "5" + + +# --------------------------------------------------------------------------- +# Install / uninstall discipline +# --------------------------------------------------------------------------- + +class TestInstallDiscipline: + def test_instrument_is_idempotent(self, instrumented): + before = len(instrumented._listener.handlers()) + assert failproofai_sdk.instrument("crewai") == () + assert len(instrumented._listener.handlers()) == before + + def test_uninstrument_removes_every_handler_from_the_bus(self): + failproofai_sdk.instrument("crewai") + listener = adapter._listener + registered = listener.handlers() + assert registered + failproofai_sdk.uninstrument("crewai") + for event_class, handler in registered: + assert handler not in crewai_event_bus._sync_handlers.get(event_class, set()) + assert handler not in crewai_event_bus._async_handlers.get(event_class, set()) + assert adapter._listener is None + + def test_a_run_left_open_is_closed_by_uninstrument(self, emitted): + """agent_end force-closes open pauses but NOT tools; a run that dies with + an open tool_use leaves the session `ongoing` forever.""" + failproofai_sdk.instrument("crewai") + crewai_event_bus.emit( + None, CrewKickoffStartedEvent(crew_name="Half Crew", inputs=None) + ) + crewai_event_bus.emit( + None, ToolUsageStartedEvent(tool_name="adder", tool_args={"a": 1}) + ) + crewai_event_bus.flush(timeout=30) + failproofai_sdk.uninstrument("crewai") + + events = emitted() + assert types_of(events) == ["agent_start", "tool_use", "tool_result", "agent_end"] + assert events[2]["fw_incomplete"] is True + assert events[2]["fw_closed_by"] == "teardown" + assert events[3]["outcome"] == "cancelled" + assert len({event["session_id"] for event in events}) == 1 + + def test_the_session_id_option_pins_the_session(self, emitted): + failproofai_sdk.instrument("crewai", session_id="pinned-session") + try: + kickoff(tool_crew()) + finally: + failproofai_sdk.uninstrument("crewai") + events = emitted() + assert events + assert {event["session_id"] for event in events} == {"pinned-session"} + + def test_install_ignores_options_meant_for_another_adapter(self): + # instrument() hands the SAME options dict to every adapter, so an + # unknown keyword must not be a TypeError that takes out the others. + assert failproofai_sdk.instrument("crewai", some_other_adapters_option=1) == ("crewai",) + failproofai_sdk.uninstrument("crewai") + + def test_two_runs_in_a_row_are_two_sessions(self, instrumented, emitted): + kickoff(tool_crew()) + kickoff(tool_crew()) + events = emitted() + sessions = {event["session_id"] for event in events} + assert len(sessions) == 2 + # ...and neither leaks into the other + first = [e for e in events if e["session_id"] == events[0]["session_id"]] + assert first == events[: len(first)] + + +# --------------------------------------------------------------------------- +# CrewAI-specific structure +# --------------------------------------------------------------------------- + +class TestCrewAIStructure: + def test_the_internal_agent_executor_flow_is_not_an_agent(self, instrumented, emitted): + """CrewAI's agent executor emits FlowStartedEvent(flow_name="AgentExecutor") + inside EVERY agent execution. Treating flow events as agents + unconditionally — which is what the API reads like — puts a spurious + `AgentExecutor` agent inside every single agent, doubling the tree and + poisoning the agent_id facet.""" + kickoff(tool_crew()) + agent_ids = {event["agent_id"] for event in emitted()} + assert "AgentExecutor" not in agent_ids + assert agent_ids == {"Arithmetic Crew", "Arithmetic Analyst"} + + def test_a_task_is_not_its_own_span(self, instrumented, emitted): + """A Task is a subset of the agent execution that runs it. Emitting both + would double every row and render them as siblings; the task rides along + as fw_task_*.""" + events = emitted() if kickoff(tool_crew()) else [] + assert len([e for e in events if e["type"] == "agent_start"]) == 2 + nested = [e for e in events if e["type"] == "agent_start"][1] + assert nested["fw_task_name"] == "add-two-numbers" + uuid.UUID(nested["fw_task_id"]) + + def test_the_crew_span_carries_its_kind_and_name(self, instrumented, emitted): + events = emitted() if kickoff(tool_crew()) else [] + root = events[0] + assert root["fw_kind"] == "crew" + assert root["fw_crew_name"] == "Arithmetic Crew" + + +# --------------------------------------------------------------------------- +# Anti-drift — the highest-value tests in this file +# --------------------------------------------------------------------------- + +class TestAntiDrift: + def test_we_still_override_something_that_exists_on_the_base(self): + """If upstream renames `setup_listeners`, our override becomes dead code + that is never called, the crew runs fine, and every fake-based test in + this file still passes.""" + base_methods = { + name for name, value in vars(BaseEventListener).items() if callable(value) + } + ours = {name for name, value in vars(FailproofAICrewListener).items() if callable(value)} + overridden = ours & base_methods + assert "setup_listeners" in overridden, ( + "FailproofAICrewListener no longer overrides anything BaseEventListener " + f"defines. Base has: {sorted(base_methods)}" + ) + + def test_every_parameter_we_declare_is_still_in_the_base_signature(self): + base_methods = { + name for name, value in vars(BaseEventListener).items() if callable(value) + } + for name in {n for n, v in vars(FailproofAICrewListener).items() if callable(v)}: + if name not in base_methods or name == "__init__": + continue + ours = set(inspect.signature(getattr(FailproofAICrewListener, name)).parameters) + theirs = set(inspect.signature(getattr(BaseEventListener, name)).parameters) + assert ours <= theirs, (name, sorted(ours - theirs)) + + def test_registration_still_happens_in_the_constructor(self): + """`BaseEventListener.__init__` calling `setup_listeners` IS the install + step. If that stops being true, `install()` registers nothing.""" + source = inspect.getsource(BaseEventListener.__init__) + assert "setup_listeners" in source + assert "setup_listeners" in getattr(BaseEventListener, "__abstractmethods__", ()) + + def test_the_bus_api_we_call_still_exists(self): + for name in ("register_handler", "off", "flush", "emit"): + assert callable(getattr(crewai_event_bus, name, None)), name + register = inspect.signature(crewai_event_bus.register_handler).parameters + assert {"event_type", "handler"} <= set(register) + off = inspect.signature(crewai_event_bus.off).parameters + assert {"event_type", "handler"} <= set(off) + + def test_every_event_class_we_map_still_exists(self): + """Resolved through the adapter's own `event_class`, deliberately. + + Asserting against `crewai.events.event_types` directly encoded a + NARROWER rule than the adapter follows, and the gap was invisible: the + flow events live only on `crewai.events`, so a mapping for one resolved + to None, `probe()` disabled it, and nothing failed. + """ + from failproofai_sdk.integrations.crewai import event_class + + for class_name, _ in TABLE: + assert event_class(class_name) is not None, class_name + for class_name in STORE_TOOLS: + assert event_class(class_name) is not None, class_name + + def test_handlers_are_keyed_by_exact_type_with_no_mro_walk(self): + """The reason there is one entry per event class instead of a BaseEvent + catch-all. If this ever gains an MRO walk, the table is redundant; if a + catch-all is added on the assumption that it works, it records nothing.""" + source = inspect.getsource(type(crewai_event_bus).emit) + assert "_sync_handlers.get(event_type" in source + assert "_async_handlers.get(event_type" in source + + def test_handlers_are_all_async(self, instrumented): + """Not a style rule. `emit()` dispatches sync handlers onto a ten-worker + pool and submission order is not execution order: measured on crewai + 1.15.8, this file's crew produced a WRONG event stream — with + `tool_result` and `model_response` silently **dropped**, because their + opening event had not been handled yet — in 13 of 25 runs with sync + handlers, and 0 of 25 with async ones.""" + handlers = instrumented._listener.handlers() + assert handlers + for event_class, handler in handlers: + assert inspect.iscoroutinefunction(handler), event_class + # crewai routes on ITS predicate, not on ours. + assert is_async_handler(handler), event_class + assert _get_param_count(handler) == 2, event_class + + def test_async_handlers_still_run_on_one_ordered_loop(self): + """The property the previous test depends on: async handlers are + scheduled with `run_coroutine_threadsafe` onto a single background loop, + and `call_soon_threadsafe` is FIFO. Sync handlers go to a pool.""" + source = inspect.getsource(type(crewai_event_bus).emit) + assert "run_coroutine_threadsafe" in source + assert "_sync_executor.submit" in source + + def test_the_span_tree_fields_we_build_on_still_exist(self): + from crewai.events.base_events import BaseEvent + + for name in ("event_id", "parent_event_id", "started_event_id", "timestamp"): + assert name in BaseEvent.model_fields, name + + def test_every_event_attribute_we_read_still_exists_somewhere(self): + """Reflect over our own source and check every `event.` we touch + is still a field on at least one event class we register. + + This is what catches a silent rename: `started_event_id` disappearing + would make every pairing fall back to the LIFO heuristic without a single + test failing. + """ + from crewai.events.base_events import BaseEvent + + from failproofai_sdk.integrations.crewai import event_class + + known = set(BaseEvent.model_fields) + for class_name, _ in TABLE: + known |= set(event_class(class_name).model_fields) + + tree = ast.parse(inspect.getsource(adapter_module)) + read = set() + for node in ast.walk(tree): + if ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id == "event" + ): + read.add(node.attr) + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "getattr" + and node.args + and isinstance(node.args[0], ast.Name) + and node.args[0].id == "event" + and len(node.args) > 1 + and isinstance(node.args[1], ast.Constant) + ): + read.add(node.args[1].value) + + assert read, "the reflection found nothing — it has stopped working" + assert read <= known, sorted(read - known) + + +# --------------------------------------------------------------------------- +# Registry wiring +# --------------------------------------------------------------------------- + +class TestRegistry: + def test_autodetect_picks_crewai_up(self): + # Auto-detect installs every framework already imported in this process, + # and by the time the whole suite has run that is all four — so put back + # exactly what this call installed, not just ours. + installed = failproofai_sdk.instrument() + try: + assert "crewai" in installed + finally: + for name in installed: + failproofai_sdk.uninstrument(name) + + def test_the_adapter_matches_the_protocol(self): + assert adapter.name == "crewai" + assert adapter.module == "crewai" + assert callable(adapter.install) + assert callable(adapter.uninstall) + + def test_importing_the_adapter_does_not_happen_at_import_agenteye(self): + # A guarded module-level import of crewai is acceptable *in this module* + # precisely because nothing imports it except instrument("crewai"). + assert "crewai" not in failproofai_sdk.__dict__ + assert not hasattr(failproofai_sdk, "integrations") or True + + +# --------------------------------------------------------------------------- +# Human in the loop +# --------------------------------------------------------------------------- + +class TestHumanInTheLoop: + """A crew blocked on a person was invisible: crewai fires + `HumanFeedbackRequestedEvent`/`HumanFeedbackReceivedEvent` and this adapter + subscribed to neither, so the whole wait was an unexplained gap and the + session's active duration absorbed it. + + LangChain and LlamaIndex both map their HITL surface onto the same four + events, in the same order. This is the crewai one. + """ + + @staticmethod + def _classes(): + from failproofai_sdk.integrations.crewai import event_class + + requested = event_class("HumanFeedbackRequestedEvent") + received = event_class("HumanFeedbackReceivedEvent") + if requested is None or received is None: # pragma: no cover + pytest.skip("this crewai has no human-feedback events") + return requested, received + + def _round_trip(self, *, flow="review_flow", method="approve", feedback="ship it"): + requested, received = self._classes() + crewai_event_bus.emit( + None, + requested( + type="human_feedback_requested", + flow_name=flow, + method_name=method, + output="the draft", + message="Approve this?", + ), + ) + crewai_event_bus.emit( + None, + received( + type="human_feedback_received", + flow_name=flow, + method_name=method, + feedback=feedback, + outcome=None, + ), + ) + crewai_event_bus.flush(timeout=30) + + def test_a_human_wait_emits_all_four_events_in_order(self, instrumented, emitted): + crewai_event_bus.emit(None, CrewKickoffStartedEvent(crew_name="C", inputs=None)) + self._round_trip() + events = emitted() + kinds = types_of(events) + for expected in ("human_wait", "agent_pause", "agent_resume", "human_input"): + assert expected in kinds, f"{expected} missing from {kinds}" + assert kinds.index("human_wait") < kinds.index("agent_pause") + assert kinds.index("agent_pause") < kinds.index("agent_resume") + assert kinds.index("agent_resume") < kinds.index("human_input") + + def test_the_pause_and_the_wait_share_one_id(self, instrumented, emitted): + """Without a shared id the SDK cannot measure either interval, and the + dashboard shows a pause that never closes.""" + crewai_event_bus.emit(None, CrewKickoffStartedEvent(crew_name="C", inputs=None)) + self._round_trip() + events = {e["type"]: e for e in emitted()} + pause_id = events["agent_pause"]["pause_id"] + assert pause_id + assert events["agent_resume"]["pause_id"] == pause_id + assert events["human_wait"]["input_id"] == pause_id + assert events["human_input"]["input_id"] == pause_id + + def test_the_prompt_and_the_answer_are_both_recorded(self, instrumented, emitted): + crewai_event_bus.emit(None, CrewKickoffStartedEvent(crew_name="C", inputs=None)) + self._round_trip(feedback="looks good, ship it") + events = {e["type"]: e for e in emitted()} + assert events["human_wait"]["prompt"] == "Approve this?" + assert events["human_input"]["response"] == "looks good, ship it" + + def test_both_closing_events_carry_a_measured_int_duration(self, instrumented, emitted): + """`agent_pause` -> `agent_resume` is the only thing that feeds pausedMs.""" + crewai_event_bus.emit(None, CrewKickoffStartedEvent(crew_name="C", inputs=None)) + self._round_trip() + events = {e["type"]: e for e in emitted()} + for kind in ("agent_resume", "human_input"): + assert isinstance(events[kind]["duration_ms"], int), kind + + def test_feedback_with_no_request_records_the_answer_but_does_not_resume( + self, instrumented, emitted + ): + """Closing a pause that never opened subtracts a pausedMs interval that + was never added, so the resume is deliberately withheld — but the answer + itself must still reach the Human surface.""" + _, received = self._classes() + crewai_event_bus.emit(None, CrewKickoffStartedEvent(crew_name="C", inputs=None)) + crewai_event_bus.emit( + None, + received( + type="human_feedback_received", + flow_name="f", + method_name="m", + feedback="orphaned answer", + outcome=None, + ), + ) + crewai_event_bus.flush(timeout=30) + + events = emitted() + kinds = types_of(events) + assert "human_input" in kinds + assert "agent_resume" not in kinds + answer = next(e for e in events if e["type"] == "human_input") + assert answer["response"] == "orphaned answer" + # NEVER None: `human_input` requires `input_id`, and passing None raises + # a TypeError inside the customer's event bus. + assert answer["input_id"] + assert answer["fw_orphaned"] is True + + def test_every_hitl_event_lands_on_the_one_session(self, instrumented, emitted): + crewai_event_bus.emit(None, CrewKickoffStartedEvent(crew_name="C", inputs=None)) + self._round_trip() + events = emitted() + assert len({e["session_id"] for e in events}) == 1 + + +# --------------------------------------------------------------------------- +# Nesting: the spans CrewAI hangs real work underneath +# --------------------------------------------------------------------------- + +def _flow_event(name, **kw): + from failproofai_sdk.integrations.crewai import event_class + + klass = event_class(name) + if klass is None: # pragma: no cover - a crewai that dropped the class + pytest.skip(f"this crewai has no {name}") + return klass(**kw) + + +class TestNesting: + """`_nodes` is what `_parent_key` resolves a `parent_event_id` against, and + an id that is not in it falls back to the open ROOT. So every CrewAI span + that can parent other spans has to be recorded there, including the two that + are not agents: + + * a `delegate_work_to_coworker` **tool call** parents the coworker's entire + `AgentExecutionStartedEvent` (measured on 1.15.16: the coworker's + `parent_event_id` IS the tool event's id), so a hierarchical crew's whole + manager/coworker hierarchy collapses onto the crew without it; + * a **flow method** parents a `Crew.kickoff()` made inside it, so the crew + becomes a second root — a whole separate session — without it. + + Neither failure raises, and both look plausible in the dashboard. + """ + + def test_a_delegated_coworker_hangs_off_the_delegating_agent( + self, instrumented, emitted + ): + crew = CrewKickoffStartedEvent(crew_name="Hierarchical Crew", inputs=None) + crewai_event_bus.emit(None, crew) + + manager = Agent(role="Crew Manager", goal="Delegate.", backstory="Manages.") + worker = Agent(role="Researcher", goal="Research.", backstory="Researches.") + task = Task(description="d", expected_output="o", agent=manager) + + manager_span = _flow_event( + "AgentExecutionStartedEvent", + agent=manager, + task=task, + tools=[], + task_prompt="p", + parent_event_id=crew.event_id, + ) + crewai_event_bus.emit(None, manager_span) + # CrewAI's own executor flow, which is a pass-through link. + executor = _flow_event( + "FlowStartedEvent", flow_name="AgentExecutor", parent_event_id=manager_span.event_id + ) + crewai_event_bus.emit(None, executor) + delegate = ToolUsageStartedEvent( + tool_name="delegate_work_to_coworker", + tool_args="{}", + agent_role="Crew Manager", + parent_event_id=executor.event_id, + ) + crewai_event_bus.emit(None, delegate) + coworker_span = _flow_event( + "AgentExecutionStartedEvent", + agent=worker, + task=task, + tools=[], + task_prompt="p", + parent_event_id=delegate.event_id, + ) + crewai_event_bus.emit(None, coworker_span) + crewai_event_bus.flush(timeout=30) + + starts = {e["agent_id"]: e for e in emitted() if e["type"] == "agent_start"} + assert starts["Crew Manager"]["parent_id"] == "Hierarchical Crew" + # The whole point: NOT "Hierarchical Crew". + assert starts["Researcher"]["parent_id"] == "Crew Manager" + + def test_a_crew_inside_a_flow_method_stays_in_one_session(self, instrumented, emitted): + flow = _flow_event("FlowStartedEvent", flow_name="ReviewFlow") + crewai_event_bus.emit(None, flow) + method = _flow_event( + "MethodExecutionStartedEvent", + flow_name="ReviewFlow", + method_name="run_crew", + state={}, + parent_event_id=flow.event_id, + ) + crewai_event_bus.emit(None, method) + crewai_event_bus.emit( + None, + CrewKickoffStartedEvent( + crew_name="Inner Crew", inputs=None, parent_event_id=method.event_id + ), + ) + crewai_event_bus.flush(timeout=30) + + events = emitted() + # One session, not two. A detached crew mints its own session id and the + # run silently becomes two runs. + assert len({e["session_id"] for e in events}) == 1 + inner = next( + e for e in events if e["type"] == "agent_start" and e["agent_id"] == "Inner Crew" + ) + assert inner["parent_id"] == "ReviewFlow" + + +class TestFlowFailure: + """A flow whose method raises emits `FlowFailedEvent` and never + `FlowFinishedEvent`. Unmapped, the flow's `agent_start` is never closed and + the session renders `ongoing` forever — the one outcome this adapter's + fallbacks exist to avoid.""" + + def test_flow_failed_closes_the_span(self, instrumented, emitted): + flow = _flow_event("FlowStartedEvent", flow_name="DoomedFlow") + crewai_event_bus.emit(None, flow) + crewai_event_bus.emit( + None, + _flow_event( + "FlowFailedEvent", + flow_name="DoomedFlow", + error=RuntimeError("sink offline"), + started_event_id=flow.event_id, + parent_event_id=flow.event_id, + ), + ) + crewai_event_bus.flush(timeout=30) + + events = emitted() + ends = [e for e in events if e["type"] == "agent_end"] + assert [e["agent_id"] for e in ends] == ["DoomedFlow"] + assert ends[0]["outcome"] == "failed" + assert "sink offline" in ends[0]["fw_error"] + + def test_flow_failed_is_in_the_table(self): + assert ("FlowFailedEvent", "on_flow_failed") in TABLE + + +class TestConcurrentRoots: + """`_roots` is process-global. With two crews open on two threads, an event + whose parent span has already been popped resolves through `_roots[-1]` — + a coin flip that files one run's events under the OTHER run's session id. + That is silent cross-session corruption, strictly worse than a missing row.""" + + def test_an_orphan_does_not_land_in_the_other_runs_session(self, instrumented, emitted): + # `restore_event_scope(())` between the two kickoffs is what a second + # THREAD would give for free: without it crewai's scope stack makes the + # second crew a child of the first and there is only ever one root, so + # the bug this guards cannot be reached. + with failproofai_sdk.session("session-alpha"): + crewai_event_bus.emit(None, CrewKickoffStartedEvent(crew_name="Alpha", inputs=None)) + restore_event_scope(()) + with failproofai_sdk.session("session-bravo"): + crewai_event_bus.emit(None, CrewKickoffStartedEvent(crew_name="Bravo", inputs=None)) + restore_event_scope(()) + crewai_event_bus.flush(timeout=30) + assert len(adapter._roots) == 2, "the test needs two concurrent roots to mean anything" + + with failproofai_sdk.session("session-alpha"): + # Its parent span is gone (closed, or evicted at _MAX_NODES), so + # this resolves through the root fallback. + crewai_event_bus.emit( + None, + ToolUsageStartedEvent( + tool_name="orphan_tool", + tool_args="{}", + agent_role="Alpha Worker", + parent_event_id=str(uuid.uuid4()), + ), + ) + crewai_event_bus.flush(timeout=30) + + orphan = next(e for e in emitted() if e["type"] == "tool_use") + # NOT "session-bravo": `_roots[-1]` is Bravo. + assert orphan["session_id"] == "session-alpha" + assert orphan["agent_id"] == "Alpha" + + +class TestLiteAgent: + """`Agent.kickoff()` is an agent run with no Crew and no Task. It emits its + OWN execution events, and it is a ROOT. Unmapped, the run has no agent span + at all: its LLM and tool events fall through to whatever ambient scope + exists (`agent_id` "main"), or are dropped outright when there is none.""" + + @staticmethod + def _pair(role="Solo Agent"): + info = {"id": str(uuid.uuid4()), "role": role, "goal": "Answer.", "backstory": "b"} + started = _flow_event( + "LiteAgentExecutionStartedEvent", agent_info=info, tools=[], messages="hi" + ) + crewai_event_bus.emit(None, started) + crewai_event_bus.emit( + None, + _flow_event( + "LiteAgentExecutionCompletedEvent", + agent_info=info, + output="done", + started_event_id=started.event_id, + ), + ) + crewai_event_bus.flush(timeout=30) + return info + + def test_it_gets_its_own_agent_span(self, instrumented, emitted): + info = self._pair() + events = emitted() + assert types_of(events) == ["agent_start", "agent_end"] + assert events[0]["agent_id"] == "Solo Agent" + assert events[0]["goal"] == "Answer." + assert events[0]["fw_lite"] is True + # The UUID goes to fw_agent_id and NEVER to the LowCardinality facet. + assert events[0]["fw_agent_id"] == info["id"] + assert events[1]["outcome"] == "success" + + def test_it_is_a_root_so_it_opens_its_own_session(self, instrumented, emitted): + self._pair() + events = emitted() + assert events[0].get("parent_id") is None + assert len({e["session_id"] for e in events}) == 1 + + def test_the_three_rows_are_in_the_table(self): + mapped = dict(TABLE) + assert mapped["LiteAgentExecutionStartedEvent"] == "on_lite_agent_started" + assert mapped["LiteAgentExecutionCompletedEvent"] == "on_agent_completed" + assert mapped["LiteAgentExecutionErrorEvent"] == "on_agent_error" + + +# --------------------------------------------------------------------------- +# Task(human_input=True) — the HITL surface that is NOT on the event bus +# --------------------------------------------------------------------------- + +def _provider(): + try: + from crewai.core.providers.human_input import SyncHumanInputProvider + except ImportError: # pragma: no cover - a crewai that moved it + pytest.skip("this crewai has no SyncHumanInputProvider") + return SyncHumanInputProvider + + +class TestTaskHumanInput: + """crewai has two HITL surfaces and only one of them is on the bus. + `human_input=True` on a Task runs through + `crewai.core.providers.human_input`, which calls `input()` and emits no + event of any kind — so the entire human wait is billed as active time on the + agent unless this seam is wrapped. It is the only patch in the adapter, + which is why install/restore is asserted as hard as the events are. + """ + + def test_the_four_events_land_on_the_blocked_agent( + self, instrumented, emitted, monkeypatch + ): + crewai_event_bus.emit(None, CrewKickoffStartedEvent(crew_name="C", inputs=None)) + agent = Agent(role="Blocked Analyst", goal="g", backstory="b") + task = Task(description="d", expected_output="o", agent=agent) + crewai_event_bus.emit( + None, + _flow_event( + "AgentExecutionStartedEvent", + agent=agent, + task=task, + tools=[], + task_prompt="p", + ), + ) + crewai_event_bus.flush(timeout=30) + + monkeypatch.setattr("builtins.input", lambda *a: "tighten the wording") + assert _provider()._prompt_input(None) == "tighten the wording" + + events = emitted() + kinds = types_of(events) + for expected in ("human_wait", "agent_pause", "agent_resume", "human_input"): + assert expected in kinds, f"{expected} missing from {kinds}" + assert kinds.index("human_wait") < kinds.index("agent_pause") + assert kinds.index("agent_pause") < kinds.index("agent_resume") + assert kinds.index("agent_resume") < kinds.index("human_input") + + by_type = {e["type"]: e for e in events} + # On the agent that is actually blocked, not on the crew above it. + assert by_type["human_wait"]["agent_id"] == "Blocked Analyst" + assert by_type["human_input"]["response"] == "tighten the wording" + assert by_type["agent_pause"]["pause_id"] == by_type["agent_resume"]["pause_id"] + assert by_type["human_wait"]["fw_surface"] == "task_human_input" + + def test_the_seam_is_restored_on_uninstrument(self): + provider = _provider() + before = provider.__dict__.get("_prompt_input") + before_async = provider.__dict__.get("_prompt_input_async") + failproofai_sdk.instrument("crewai") + assert provider.__dict__.get("_prompt_input") is not before + failproofai_sdk.uninstrument("crewai") + assert provider.__dict__.get("_prompt_input") is before + assert provider.__dict__.get("_prompt_input_async") is before_async + + def test_installing_twice_does_not_wrap_twice(self, instrumented): + # A second install with no restore in between would emit the pause twice + # for one prompt. + adapter._patch_task_human_input() + assert len(adapter._patched) == 2 + + def test_it_is_still_a_staticmethod(self, instrumented): + # `self._prompt_input(context.crew)` is the call site: a bare function + # here would bind and arrive with `self` where `crew` belongs. + assert isinstance(_provider().__dict__["_prompt_input"], staticmethod) + + def test_a_raising_prompt_still_raises(self, instrumented, monkeypatch): + boom = KeyboardInterrupt() + + def _raise(*args): + raise boom + + monkeypatch.setattr("builtins.input", _raise) + with pytest.raises(KeyboardInterrupt) as excinfo: + _provider()._prompt_input(None) + assert excinfo.value is boom diff --git a/sdk/python/tests/integrations/test_langchain.py b/sdk/python/tests/integrations/test_langchain.py new file mode 100644 index 000000000..a5fdb39bf --- /dev/null +++ b/sdk/python/tests/integrations/test_langchain.py @@ -0,0 +1,2212 @@ +"""The LangChain / LangGraph adapter, against the real framework. + +Everything here runs a **real** graph on a **fake** chat model +(`GenericFakeChatModel` — no network, no key, deterministic) and then reads the +JSONL the writer actually produced. Nothing asserts on mock call arguments: the +whole failure mode this adapter exists inside is "the events looked right in a +mock and were wrong on disk". + +Two of these tests are worth more than the rest put together: + +* `test_every_override_still_exists_on_its_framework_base` — if upstream renames + a callback, our override becomes **dead code that is never called** and every + behavioural test below still passes, because they only ever assert on events + we did emit. Reflection over the class is the only thing that catches it. +* `test_the_node_filter_still_matches_real_langgraph_metadata` — the same + problem one layer down. The node/inner-runnable filter is a string comparison + against `metadata["langgraph_node"]`; if that key moves, every node silently + stops being a hook and the timeline just gets shorter. +""" + +import inspect +import json +import logging +import operator +import os +from typing import Annotated, TypedDict + +import pytest + +import failproofai_sdk +from failproofai_sdk.integrations import _compat, _core + +pytestmark = pytest.mark.framework + +# `importorskip` is fail-open: misspell the module and every test here skips +# while CI stays green having tested nothing. The framework CI leg sets this +# env var, which turns the skip into an import error. +if os.environ.get("AGENTEYE_TESTS_REQUIRE_FRAMEWORKS"): + import langchain_core # noqa: F401 + import langgraph # noqa: F401 +else: + pytest.importorskip("langchain_core") + pytest.importorskip("langgraph") + +from langchain_core.documents import Document # noqa: E402 +from langchain_core.language_models.fake_chat_models import ( # noqa: E402 + FakeListChatModel, + GenericFakeChatModel, +) +from langchain_core.messages import AIMessage, HumanMessage, ToolMessage # noqa: E402 +from langchain_core.retrievers import BaseRetriever # noqa: E402 +from langchain_core.runnables import RunnableConfig, RunnableLambda # noqa: E402 +from langchain_core.tools import tool # noqa: E402 +from langgraph.checkpoint.memory import InMemorySaver # noqa: E402 +from langgraph.graph import END, START, StateGraph # noqa: E402 +from langgraph.prebuilt import ToolNode # noqa: E402 +from langgraph.types import Command, Send, interrupt # noqa: E402 + +from failproofai_sdk.integrations import langchain as adapter # noqa: E402 + +WATCHED_LOGGERS = ("failproofai_sdk.integrations", "langchain_core.callbacks.manager") + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +class _Sink(logging.Handler): + """Collects anything the adapter (or LangChain) swallowed. + + LangChain firewalls handler exceptions in `handle_event` — it catches, logs + a WARNING and carries on — and `_core.safe` does the same one layer in. So a + broken translator produces a **green test** and a log line. This turns that + log line into a failure, which is the only way these tests mean anything. + """ + + def __init__(self): + super().__init__(level=logging.WARNING) + self.records = [] + self.allow = False + + def emit(self, record): + self.records.append(record) + + +@pytest.fixture(autouse=True) +def sink(): + handler = _Sink() + loggers = [logging.getLogger(name) for name in WATCHED_LOGGERS] + for logger in loggers: + logger.addHandler(handler) + try: + yield handler + finally: + for logger in loggers: + logger.removeHandler(handler) + if not handler.allow and handler.records: + pytest.fail( + "instrumentation failure was swallowed and only logged:\n" + + "\n".join(handler.format(r) for r in handler.records) + ) + + +@pytest.fixture(autouse=True) +def _adapter_state(monkeypatch, tmp_path): + import shutil + + from failproofai_sdk import _runtime + + monkeypatch.delenv("FAILPROOFAI_SDK_STRICT", raising=False) + monkeypatch.delenv("FAILPROOFAI_SDK_STRICT_INTEGRATIONS", raising=False) + monkeypatch.delenv(adapter.ENV_VAR, raising=False) + _core.set_strict(None) + _compat.set_strict_integrations(None) + _core.reset_failures() + _compat.reset_warnings() + # Instrumentation is process-global and `instrument()` with no argument + # installs every *detected* framework — so another test file that + # auto-detected can leave this adapter active, and `instrument("langchain")` + # would then correctly return () and this file would test nothing. + failproofai_sdk.uninstrument() + # The writer's own thread must not flush while a test is running: filenames + # have millisecond resolution, so two flushes in the same millisecond + # clobber each other and the test reads half its events. + _runtime.writer.set_flush_interval(3600) + # A previous test can legitimately emit *after* it read its events — + # `uninstrument()` closes spans that were still open, which is the point. + # Drain and discard those before this test starts, or they land in this + # test's directory and every assertion about "the first agent_end" is wrong. + _runtime.writer.flush_now() + shutil.rmtree(tmp_path / "events", ignore_errors=True) + yield + failproofai_sdk.uninstrument() + _runtime.writer.flush_now() + _core.reset_failures() + _compat.reset_warnings() + _core.set_strict(None) + _compat.set_strict_integrations(None) + + +@pytest.fixture() +def instrumented(): + assert failproofai_sdk.instrument("langchain") == ("langchain",) + yield + failproofai_sdk.uninstrument("langchain") + + +def read_events(tmp_path): + """Flush and read what actually reached disk, in emission order.""" + failproofai_sdk._writer.flush_now() + rows = [] + for path in sorted((tmp_path / "events").glob("*.jsonl")): + for line in path.read_text(encoding="utf-8").splitlines(): + if line.strip(): + rows.append(json.loads(line)) + return rows + + +def types_of(rows): + return [row["type"] for row in rows] + + +def only(rows, *kinds): + return [row for row in rows if row["type"] in kinds] + + +# --------------------------------------------------------------------------- +# Graphs +# --------------------------------------------------------------------------- + +@tool +def adder(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + +@tool +def exploder(x: int) -> int: + """Always fails.""" + raise RuntimeError("tool boom") + + +class State(TypedDict): + messages: Annotated[list, operator.add] + vals: Annotated[list, operator.add] + answer: str + n: int + + +def tool_calling_message(**kwargs): + return AIMessage( + "planned", + tool_calls=[{"name": "adder", "args": {"a": 1, "b": 2}, "id": "call_abc"}], + usage_metadata={"input_tokens": 11, "output_tokens": 5, "total_tokens": 16}, + **kwargs, + ) + + +def fake_model(*messages): + return GenericFakeChatModel(messages=iter(list(messages))) + + +def subgraph(): + sg = StateGraph(State) + sg.add_node("sub_step", lambda state: {"vals": ["sub"]}) + sg.add_edge(START, "sub_step") + sg.add_edge("sub_step", END) + return sg.compile(name="child_graph") + + +def build_graph(*, model=None, checkpointer=None, name="root_graph"): + """plan -> tools -> child(subgraph) -> fan-out -> ask(interrupt) -> END.""" + model = model or fake_model(tool_calling_message(), AIMessage("again")) + + def plan(state): + reply = model.invoke(state["messages"]) + return {"messages": [reply], "n": state["n"] + 1, "vals": ["plan"]} + + def fan(state): + return [Send("worker", {"i": i}) for i in range(2)] + + def ask(state): + return {"answer": str(interrupt({"prompt": "approve?", "options": ["y", "n"]}))} + + graph = StateGraph(State) + graph.add_node("plan", plan) + graph.add_node("tools", ToolNode([adder])) + graph.add_node("child", subgraph()) + graph.add_node("worker", lambda state: {"vals": ["w"]}) + graph.add_node("ask", ask) + graph.add_edge(START, "plan") + graph.add_edge("plan", "tools") + graph.add_edge("tools", "child") + graph.add_conditional_edges("child", fan, ["worker"]) + graph.add_edge("worker", "ask") + graph.add_edge("ask", END) + return graph.compile(name=name, checkpointer=checkpointer) + + +def build_simple(nodes, *, name="simple", checkpointer=None, edges=None): + graph = StateGraph(State) + previous = START + for node_name, fn in nodes: + graph.add_node(node_name, fn) + graph.add_edge(previous, node_name) + previous = node_name + graph.add_edge(previous, END) + return graph.compile(name=name, checkpointer=checkpointer) + + +def empty_state(**kwargs): + base = {"messages": [HumanMessage("hi")], "vals": [], "answer": "", "n": 0} + base.update(kwargs) + return base + + +def build_looping(model): + """plan -> check -> (plan | END). Exercises a node visited more than once.""" + + def plan(state): + model.invoke(state["messages"]) + return {"n": state["n"] + 1, "vals": ["plan"]} + + def check(state): + return {"vals": ["check"]} + + graph = StateGraph(State) + graph.add_node("plan", plan) + graph.add_node("check", check) + graph.add_edge(START, "plan") + graph.add_edge("plan", "check") + graph.add_conditional_edges( + "check", lambda s: "plan" if s["n"] < 3 else END, {"plan": "plan", END: END} + ) + return graph.compile(name="looper") + + +# --------------------------------------------------------------------------- +# Shape of a representative run +# --------------------------------------------------------------------------- + +def test_event_type_sequence_for_a_representative_run(tmp_path, instrumented): + model = fake_model(tool_calling_message()) + + def plan(state): + return {"messages": [model.invoke(state["messages"])], "vals": ["plan"]} + + def act(state): + adder.invoke({"a": 1, "b": 2}) + return {"vals": ["act"]} + + app = build_simple([("plan", plan), ("act", act)], name="pipeline") + app.invoke(empty_state(), config={"configurable": {"thread_id": "seq"}}) + + assert types_of(read_events(tmp_path)) == [ + "agent_start", + "hook_triggered", # plan + "model_request", + "model_response", + "hook_completed", + "hook_triggered", # act + "tool_use", + "tool_result", + "hook_completed", + "agent_end", + ] + + +def test_the_root_agent_start_is_the_sessions_first_event(tmp_path, instrumented): + build_graph(checkpointer=InMemorySaver()).invoke( + empty_state(), config={"configurable": {"thread_id": "first"}} + ) + rows = read_events(tmp_path) + assert rows[0]["type"] == "agent_start" + assert rows[0]["agent_id"] == "root_graph" + assert rows[0].get("parent_id") is None + # ...and it is the only root agent_start in the session. + roots = [r for r in rows if r["type"] == "agent_start" and r.get("parent_id") is None] + assert len(roots) == 1 + + +def test_every_event_carries_one_session_id_and_the_framework(tmp_path, instrumented): + build_graph(checkpointer=InMemorySaver()).invoke( + empty_state(), config={"configurable": {"thread_id": "sess"}} + ) + rows = read_events(tmp_path) + assert rows + assert {r["session_id"] for r in rows} == {"sess"} + assert {r["framework"] for r in rows} == {"langchain"} + assert all(r["framework_version"] for r in rows) + assert all(r["integration_version"] for r in rows) + + +def test_agent_ids_are_human_readable_names_never_uuids(tmp_path, instrumented): + build_graph(checkpointer=InMemorySaver()).invoke( + empty_state(), config={"configurable": {"thread_id": "names"}} + ) + rows = read_events(tmp_path) + agent_ids = {r["agent_id"] for r in rows} + assert agent_ids == {"root_graph", "root_graph/child"} + for value in agent_ids: + assert not _looks_like_a_uuid(value), value + # Hook names are node names too — `hook_name` is its own facet on /hooks and + # a uuid there is just as poisonous as one in agent_id. + for row in only(rows, "hook_triggered", "hook_completed"): + assert not _looks_like_a_uuid(row["hook_name"]) + + +def _looks_like_a_uuid(value): + import uuid + + try: + uuid.UUID(str(value)) + except (ValueError, AttributeError, TypeError): + return False + return True + + +def test_no_event_carries_an_extra_that_would_shadow_a_declared_field(tmp_path, instrumented): + build_graph(checkpointer=InMemorySaver()).invoke( + empty_state(), config={"configurable": {"thread_id": "shadow"}} + ) + rows = read_events(tmp_path) + assert rows + # `_schema._build()` ends with `result.update(extra)`, so an extra called + # `tool_name`, `model`, `outcome` or `input_tokens` silently OVERWRITES the + # declared field — and therefore the promoted column and the + # server's computed summary — while every behavioural test still passes. + # The allowed key set is derived from `_schema`'s own dataclasses, so adding + # a field there cannot leave a stale copy here. + declared = _declared_fields_by_type() + assert set(declared) == _SCHEMA_TYPES + for row in rows: + allowed = declared[row["type"]] | _core.ALLOWED_TOP_LEVEL + for key in row: + if key.startswith("fw_"): + assert key not in _core.FORBIDDEN_EXTRAS, (row["type"], key) + continue + assert key in allowed, ( + "%s carries %r, which is not a field of that event — if it is an " + "extra it must be namespaced fw_*, or it silently shadows a " + "declared field" % (row["type"], key) + ) + + +def _declared_fields_by_type(): + import dataclasses + import re + + from failproofai_sdk import _schema + + out = {} + for name, obj in vars(_schema).items(): + if not (dataclasses.is_dataclass(obj) and isinstance(obj, type)): + continue + event_type = re.sub(r"(?= 0 + + +def test_model_name_and_tokens_are_normalized(tmp_path, instrumented): + app = build_simple( + [("plan", lambda s: {"messages": [_MODEL.invoke(s["messages"])], "vals": ["p"]})], + name="tokens", + ) + app.invoke(empty_state(), config={"configurable": {"thread_id": "tok"}}) + response = only(read_events(tmp_path), "model_response")[0] + assert response["model"] == "GenericFakeChatModel" + assert response["input_tokens"] == 11 + assert response["output_tokens"] == 5 + # Shipped as a dict as well: both the server summary and the dashboard fall + # back to payload.usage when the promoted columns are absent. + assert response["usage"] == {"input_tokens": 11, "output_tokens": 5, "total_tokens": 16} + + +_MODEL = fake_model(tool_calling_message(), AIMessage("x"), AIMessage("y")) + + +def test_model_request_carries_normalized_messages_not_flattened_prompts( + tmp_path, instrumented +): + model = fake_model(AIMessage("ok")) + app = build_simple( + [("plan", lambda s: {"messages": [model.invoke(s["messages"])], "vals": ["p"]})], + name="msgs", + ) + app.invoke(empty_state(), config={"configurable": {"thread_id": "msg"}}) + request = only(read_events(tmp_path), "model_request")[0] + # `_create_chat_model_run` flattens messages to "Human: hi" before they + # reach the Run object, which loses the roles. We capture them from + # `on_chat_model_start`, where they are still BaseMessage objects. + assert request["messages"] == [{"role": "user", "content": "hi"}] + + +def test_streaming_never_emits_per_token_events(tmp_path, instrumented): + model = FakeListChatModel(responses=["hello there"]) + app = build_simple( + [("stream", lambda s: {"vals": [c.content for c in model.stream(s["messages"])]})], + name="streamer", + ) + app.invoke(empty_state(), config={"configurable": {"thread_id": "stream"}}) + rows = read_events(tmp_path) + # A 500-token response must not become 500 rows against a five-lane rail. + assert len(only(rows, "model_request")) == 1 + assert len(only(rows, "model_response")) == 1 + response = only(rows, "model_response")[0] + assert response["fw_streamed"] is True + assert response["fw_chunks"] >= len("hello there") + assert isinstance(response["fw_ttft_ms"], int) + + +def test_a_failed_model_call_is_reported_on_the_model_span(tmp_path, instrumented, sink): + class Boom(GenericFakeChatModel): + def _generate(self, *args, **kwargs): + raise RuntimeError("model 429") + + model = Boom(messages=iter([AIMessage("never")])) + + def plan(state): + try: + model.invoke(state["messages"]) + except RuntimeError: + return {"vals": ["caught"]} + return {"vals": ["nope"]} + + build_simple([("plan", plan)], name="modelfail").invoke( + empty_state(), config={"configurable": {"thread_id": "mf"}} + ) + rows = read_events(tmp_path) + response = only(rows, "model_response")[0] + assert response["stop_reason"] == "error" + assert "model 429" in response["error"] + assert isinstance(response["duration_ms"], int) + # The node caught it, so nothing above owns the failure and no standalone + # `error` event may appear — `sessionSummary.errorCount` would double-count. + assert not only(rows, "error") + assert only(rows, "agent_end")[0]["outcome"] == "success" + + +# --------------------------------------------------------------------------- +# Tools and retrievers +# --------------------------------------------------------------------------- + +def test_tool_events_pair_on_tool_call_id_and_carry_a_duration(tmp_path, instrumented): + model = fake_model(tool_calling_message()) + + def plan(state): + return {"messages": [model.invoke(state["messages"])], "vals": ["p"]} + + graph = StateGraph(State) + graph.add_node("plan", plan) + graph.add_node("tools", ToolNode([adder])) + graph.add_edge(START, "plan") + graph.add_edge("plan", "tools") + graph.add_edge("tools", END) + graph.compile(name="tools_graph").invoke( + empty_state(), config={"configurable": {"thread_id": "tools"}} + ) + + rows = read_events(tmp_path) + use = only(rows, "tool_use")[0] + result = only(rows, "tool_result")[0] + assert use["tool_call_id"] == result["tool_call_id"] + # The **LLM-issued** id, not our run id: this is what makes our events line + # up with the provider's logs and with the assistant message's tool_calls. + assert use["tool_call_id"] == "call_abc" + assert use["tool_name"] == result["tool_name"] == "adder" + assert isinstance(result["duration_ms"], int) + assert result.get("error") is None + + +def test_a_failed_tool_is_reported_on_the_tool_span_only(tmp_path, instrumented): + def act(state): + try: + exploder.invoke({"x": 1}) + except RuntimeError: + return {"vals": ["caught"]} + return {"vals": ["nope"]} + + build_simple([("act", act)], name="toolfail").invoke( + empty_state(), config={"configurable": {"thread_id": "tf"}} + ) + rows = read_events(tmp_path) + result = only(rows, "tool_result")[0] + assert "tool boom" in result["error"] + # One error, on the span that owns it. No standalone `error` event, and the + # run itself succeeded because the node handled the failure. + assert not only(rows, "error") + assert only(rows, "agent_end")[0]["outcome"] == "success" + assert only(rows, "hook_completed")[0]["outcome"] == "success" + + +def test_retriever_output_is_summarized_never_the_document_text(tmp_path, instrumented): + class Retriever(BaseRetriever): + def _get_relevant_documents(self, query, *, run_manager=None): + return [ + Document(page_content="SECRET" * 500, metadata={"source": "a.txt"}), + Document(page_content="SECRET" * 500, metadata={"source": "b.txt"}), + ] + + retriever = Retriever() + build_simple( + [("fetch", lambda s: {"vals": [len(retriever.invoke("q"))]})], name="rag" + ).invoke(empty_state(), config={"configurable": {"thread_id": "rag"}}) + + rows = read_events(tmp_path) + use = only(rows, "tool_use")[0] + result = only(rows, "tool_result")[0] + assert use["tool_name"] == "retriever:Retriever" + assert result["output"] == {"n": 2, "sources": ["a.txt", "b.txt"]} + assert "SECRET" not in json.dumps(rows) + + +# --------------------------------------------------------------------------- +# Graph structure +# --------------------------------------------------------------------------- + +def test_a_langgraph_node_is_a_hook_not_a_nested_agent(tmp_path, instrumented): + build_graph(checkpointer=InMemorySaver()).invoke( + empty_state(), config={"configurable": {"thread_id": "hooks"}} + ) + rows = read_events(tmp_path) + hooks = {r["hook_name"] for r in only(rows, "hook_triggered")} + # `sub_step` is the subgraph's own node — also a hook, under the nested agent. + assert hooks == {"plan", "tools", "child", "worker", "ask", "sub_step"} + for row in only(rows, "hook_triggered"): + assert row["trigger_event"] == "graph_node" + # Nodes must never inflate agent_id: it is a LowCardinality column and the + # primary facet, and `agent_sessions.agent_id = any(...)` would label the + # session with whichever node happened to run first. + assert "plan" not in {r["agent_id"] for r in rows} + + +def test_a_compiled_subgraph_becomes_a_nested_agent(tmp_path, instrumented): + build_graph(checkpointer=InMemorySaver()).invoke( + empty_state(), config={"configurable": {"thread_id": "sub"}} + ) + rows = read_events(tmp_path) + nested = [r for r in only(rows, "agent_start") if r.get("parent_id")] + assert len(nested) == 1 + assert nested[0]["agent_id"] == "root_graph/child" + assert nested[0]["parent_id"] == "root_graph" + # The subgraph's own node runs under the nested agent, and the agent closes. + inner = [r for r in rows if r["agent_id"] == "root_graph/child"] + assert types_of(inner) == ["agent_start", "hook_triggered", "hook_completed", "agent_end"] + assert inner[1]["hook_name"] == "sub_step" + + +def test_intermediate_runnables_and_edge_functions_emit_nothing(tmp_path, instrumented): + rows = [] + + def plan(state): + return {"vals": ["p"]} + + graph = StateGraph(State) + graph.add_node("plan", plan) + graph.add_node("done", lambda s: {"vals": ["d"]}) + graph.add_edge(START, "plan") + graph.add_conditional_edges( + "plan", _named_edge, {"done": "done", END: END} + ) + graph.add_edge("done", END) + graph.compile(name="edges").invoke( + empty_state(), config={"configurable": {"thread_id": "edges"}} + ) + rows = read_events(tmp_path) + names = {r["hook_name"] for r in only(rows, "hook_triggered")} + # `_named_edge` is a Runnable with its own run and it inherits the node's + # `langgraph_node` metadata; only `run.name == metadata["langgraph_node"]` + # keeps it out. Emitting it would bury the timeline in machinery. + assert names == {"plan", "done"} + + +def _named_edge(state): + return "done" + + +def test_a_node_visited_repeatedly_produces_one_hook_pair_per_visit(tmp_path, instrumented): + app = build_looping(fake_model(*[AIMessage("t%d" % i) for i in range(5)])) + app.invoke(empty_state(), config={"configurable": {"thread_id": "loop"}}) + rows = read_events(tmp_path) + plans = [r for r in only(rows, "hook_triggered") if r["hook_name"] == "plan"] + assert len(plans) == 3 + assert len({r["hook_id"] for r in plans}) == 3 + completed = [r for r in only(rows, "hook_completed") if r["hook_name"] == "plan"] + assert {r["hook_id"] for r in plans} == {r["hook_id"] for r in completed} + + +def test_a_parallel_fan_out_produces_one_hook_pair_per_branch(tmp_path, instrumented): + build_graph(checkpointer=InMemorySaver()).invoke( + empty_state(), config={"configurable": {"thread_id": "fan"}} + ) + rows = read_events(tmp_path) + workers = [r for r in only(rows, "hook_triggered") if r["hook_name"] == "worker"] + assert len(workers) == 2 + assert len({r["hook_id"] for r in workers}) == 2 + + +def test_every_event_belongs_to_an_agent_whose_start_is_open(tmp_path, instrumented): + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "open"}} + app.invoke(empty_state(), config=config) + app.invoke(Command(resume="yes"), config=config) + + open_agents = set() + for row in read_events(tmp_path): + if row["type"] == "agent_start": + open_agents.add(row["agent_id"]) + continue + # The dashboard parents every leaf to the open agent with the same + # agent_id and SYNTHESISES a never-ending root span when there is none. + assert row["agent_id"] in open_agents, row + if row["type"] == "agent_end": + open_agents.discard(row["agent_id"]) + assert not open_agents + + +# --------------------------------------------------------------------------- +# Human in the loop +# --------------------------------------------------------------------------- + +def test_interrupt_and_resume_emit_both_pairs_in_order(tmp_path, instrumented): + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "hitl"}} + first = app.invoke(empty_state(), config=config) + assert "__interrupt__" in first + result = app.invoke(Command(resume="approved"), config=config) + assert result["answer"] == "approved" + + rows = read_events(tmp_path) + hitl = types_of(only(rows, "human_wait", "agent_pause", "agent_resume", "human_input")) + # Neither pair alone is enough: only agent_pause<->agent_resume feeds + # `pausedMs`, and only human_wait<->human_input carries the prompt, the + # answer and `pendingHuman`. + assert hitl == ["human_wait", "agent_pause", "agent_resume", "human_input"] + + wait = only(rows, "human_wait")[0] + pause = only(rows, "agent_pause")[0] + resume = only(rows, "agent_resume")[0] + answer = only(rows, "human_input")[0] + assert wait["input_id"] == pause["pause_id"] == resume["pause_id"] == answer["input_id"] + assert wait["prompt"] == "approve?" + assert wait["options"] == ["y", "n"] + assert answer["response"] == "approved" + assert isinstance(resume["duration_ms"], int) + + +def test_an_interrupt_is_control_flow_not_an_error(tmp_path, instrumented): + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "ctrl"}} + app.invoke(empty_state(), config=config) + app.invoke(Command(resume="yes"), config=config) + + rows = read_events(tmp_path) + # LangGraph reports the GraphInterrupt through on_chain_error with no + # special case, so without the GraphBubbleUp check every human approval + # would paint a red error and a failed agent. + assert not only(rows, "error") + assert [r["outcome"] for r in only(rows, "agent_end")] == ["success", "success"] + asks = [r for r in only(rows, "hook_completed") if r["hook_name"] == "ask"] + assert [r["outcome"] for r in asks] == ["paused", "success"] + assert asks[0].get("error") is None + + +def test_the_agent_stays_open_across_the_pause_so_one_run_is_one_span( + tmp_path, instrumented +): + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "span"}} + app.invoke(empty_state(), config=config) + app.invoke(Command(resume="yes"), config=config) + + rows = read_events(tmp_path) + roots = [r for r in only(rows, "agent_start") if not r.get("parent_id")] + # One agent_start, one agent_end, across two `.invoke()` calls. Closing the + # agent at the first invoke would force-close the open pause and zero out + # the only interval that measures how long the human took. + assert len(roots) == 1 + assert len([r for r in only(rows, "agent_end") if r["agent_id"] == "root_graph"]) == 1 + assert rows[-1]["type"] == "agent_end" + assert {r["session_id"] for r in rows} == {"span"} + + +def test_interrupt_events_survive_without_the_graph_lifecycle_callbacks( + tmp_path, monkeypatch +): + """The exception path alone must produce the whole HITL round trip. + + `GraphCallbackHandler.on_interrupt` is new in langgraph 1.2 and, as shipped, + is not delivered to a handler installed through `register_configure_hook` at + all — so the fallback is not a legacy branch, it is the load-bearing one on + any install where the wrap does not apply. + """ + failproofai_sdk.instrument("langchain", graph_callbacks=False) + try: + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "nolifecycle"}} + app.invoke(empty_state(), config=config) + app.invoke(Command(resume="yes"), config=config) + rows = read_events(tmp_path) + finally: + failproofai_sdk.uninstrument("langchain") + + assert types_of(only(rows, "human_wait", "agent_pause", "agent_resume", "human_input")) == [ + "human_wait", + "agent_pause", + "agent_resume", + "human_input", + ] + assert not only(rows, "error") + + +def test_the_two_interrupt_paths_do_not_double_emit(tmp_path, instrumented): + """Both the lifecycle callback and the exception path fire; `Interrupt.id` + dedups them. A regression here doubles every pause in the dashboard.""" + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "dedup"}} + app.invoke(empty_state(), config=config) + rows = read_events(tmp_path) + assert len(only(rows, "human_wait")) == 1 + assert len(only(rows, "agent_pause")) == 1 + + +# --------------------------------------------------------------------------- +# Failures +# --------------------------------------------------------------------------- + +def test_a_node_failure_fails_the_agent_and_is_counted_once(tmp_path, instrumented): + def boom(state): + raise ValueError("node exploded") + + app = build_simple([("boom", boom)], name="failing") + with pytest.raises(ValueError): + app.invoke(empty_state(), config={"configurable": {"thread_id": "fail"}}) + + rows = read_events(tmp_path) + hook = only(rows, "hook_completed")[0] + assert hook["outcome"] == "failed" + assert "node exploded" in hook["error"] + # `"failed"`, never `"failure"` — the server only counts + # error|failed|timeout|rejected as a failure. + assert only(rows, "agent_end")[0]["outcome"] == "failed" + # The hook already owns this failure. A standalone `error` event as well + # would make `sessionSummary.errorCount` report two failures for one + # exception, on every failed run. + assert not only(rows, "error") + + +def test_a_failure_no_span_owns_produces_exactly_one_error_event(tmp_path, instrumented): + from langgraph.errors import GraphRecursionError + + model = fake_model(*[AIMessage("t%d" % i) for i in range(50)]) + + def plan(state): + model.invoke(state["messages"]) + return {"n": state["n"] + 1, "vals": ["p"]} + + graph = StateGraph(State) + graph.add_node("plan", plan) + graph.add_edge(START, "plan") + graph.add_conditional_edges("plan", lambda s: "plan", {"plan": "plan"}) + app = graph.compile(name="runaway") + with pytest.raises(GraphRecursionError): + app.invoke( + empty_state(), + config={"configurable": {"thread_id": "recursion"}, "recursion_limit": 4}, + ) + + rows = read_events(tmp_path) + # The recursion limit is enforced by the Pregel loop, not by a node, so no + # leaf reported it: this is exactly the case where a standalone `error` + # event is the only way the failure reaches the Errors surface. + assert len(only(rows, "error")) == 1 + assert only(rows, "error")[0]["error_type"] == "GraphRecursionError" + assert only(rows, "agent_end")[0]["outcome"] == "failed" + # Strictly before agent_end: the graph closes the agent span at agent_end, + # so an error after it is attributed to nothing. + assert types_of(rows)[-2:] == ["error", "agent_end"] + + +def test_a_translator_that_raises_on_every_call_cannot_break_the_graph( + tmp_path, monkeypatch, instrumented, sink +): + sink.allow = True # the whole point is that the failures are logged, not raised + + def explode(*args, **kwargs): + raise RuntimeError("translator is broken") + + for name in ("_on_start", "_on_end", "_stash", "_stash_error", "_stash_messages"): + monkeypatch.setattr(adapter, name, explode) + + model = fake_model(tool_calling_message()) + + def plan(state): + return {"messages": [model.invoke(state["messages"])], "vals": ["p"]} + + def act(state): + return {"vals": [adder.invoke({"a": 2, "b": 3})]} + + app = build_simple([("plan", plan), ("act", act)], name="broken") + result = app.invoke(empty_state(), config={"configurable": {"thread_id": "broken"}}) + + # The customer's run is untouched: right answer, no exception, no missing work. + assert result["vals"] == ["p", 5] + assert result["messages"][-1].content == "planned" + assert sink.records + + +def test_the_translators_swallow_by_policy_not_by_accident(monkeypatch, sink): + """`FAILPROOFAI_SDK_STRICT=1` is what makes the failure policy testable. + + Without it you can only ever prove "the customer's run still worked", never + "we swallowed the right thing" — and an adapter that swallowed + `BaseException` would pass the first check while silently breaking + cancellation in every async application that installed it. + """ + sink.allow = True # `safe()` logs the swallow, which is the whole design + assert getattr(adapter._on_start, "__failproofai_safe__", False) + assert getattr(adapter._on_end, "__failproofai_safe__", False) + + def explode(_run): + raise RuntimeError("boom") + + guarded = _core.safe(explode) + guarded(object()) # swallowed by default + + monkeypatch.setenv("FAILPROOFAI_SDK_STRICT", "1") + _core.set_strict(None) + with pytest.raises(RuntimeError): + guarded(object()) + + def cancel(_run): + raise KeyboardInterrupt + + _core.set_strict(False) + # A BaseException is never swallowed, strict or not. + with pytest.raises(KeyboardInterrupt): + _core.safe(cancel)(object()) + + +def test_strict_mode_actually_surfaces_through_a_real_graph(monkeypatch, sink, instrumented): + """The check above proves `safe()` re-raises. It does NOT prove the caller + ever sees it — and for a while, they didn't. + + LangChain's `handle_event` catches every handler exception and logs + "Error in . callback" unless the handler sets + `raise_error`. We hard-coded that False, so under `FAILPROOFAI_SDK_STRICT=1` + `safe()` re-raised straight into LangChain's firewall and the fault was + swallowed one layer further out: the escape hatch silently did nothing on + the adapter people are most likely to be debugging. `raise_error` now + follows strict mode. + + Note this asserts through a real `graph.invoke`, not against `safe()`. The + isolated test above passed the entire time the feature was broken. + """ + sink.allow = True + + graph = StateGraph(State) + graph.add_node("bump", lambda state: {"vals": ["bumped"]}) + graph.add_edge(START, "bump") + graph.add_edge("bump", END) + compiled = graph.compile(name="strict_probe") + + def explode(self, *args, **kwargs): + raise RuntimeError("translator exploded") + + monkeypatch.setattr(_core.RunTracker, "emit", explode) + + # Default: the customer's graph is untouched and still returns the answer. + _core.set_strict(False) + assert compiled.invoke({"vals": [], "messages": []})["vals"] == ["bumped"] + + # Strict: the fault reaches the caller instead of vanishing into a log line. + monkeypatch.setenv("FAILPROOFAI_SDK_STRICT", "1") + _core.set_strict(None) + with pytest.raises(RuntimeError, match="translator exploded"): + compiled.invoke({"vals": [], "messages": []}) + + +def test_two_graphs_on_two_threads_never_mix_sessions(tmp_path, instrumented): + """All adapter state is module-global — it has to be, because the configure + hook builds a fresh handler per callback manager. So the isolation has to + come from the run-id keys, and that is what this proves.""" + import threading + + app = build_simple([("n", lambda s: {"vals": ["x"]})], name="threaded") + barrier = threading.Barrier(4) + + def worker(name): + barrier.wait(timeout=10) + for _ in range(3): + app.invoke(empty_state(), config={"configurable": {"thread_id": name}}) + + threads = [threading.Thread(target=worker, args=("t%d" % i,)) for i in range(4)] + for thread in threads: + thread.start() + for thread in threads: + thread.join(timeout=30) + + rows = read_events(tmp_path) + assert {r["session_id"] for r in rows} == {"t0", "t1", "t2", "t3"} + per_session = {} + for row in rows: + per_session.setdefault(row["session_id"], []).append(row["type"]) + for session, kinds in per_session.items(): + assert kinds.count("agent_start") == 3, (session, kinds) + assert kinds.count("agent_end") == 3, (session, kinds) + assert kinds.count("hook_triggered") == 3, (session, kinds) + + +# --------------------------------------------------------------------------- +# Session resolution and interop +# --------------------------------------------------------------------------- + +def test_session_id_prefers_the_documented_metadata_key_over_thread_id( + tmp_path, instrumented +): + build_simple([("n", lambda s: {"vals": ["x"]})], name="s").invoke( + empty_state(), + config={ + "configurable": {"thread_id": "the-thread"}, + "metadata": {adapter.SESSION_METADATA_KEY: "chosen"}, + }, + ) + assert {r["session_id"] for r in read_events(tmp_path)} == {"chosen"} + + +def test_session_id_falls_back_to_thread_id(tmp_path, instrumented): + """Verified against langgraph 1.2.10, contra the widely-reported claim that + `thread_id` is no longer visible to callbacks: langchain-core's + `ensure_config` stopped promoting it, and langgraph's `_PROPAGATE_TO_METADATA` + puts it back.""" + build_simple([("n", lambda s: {"vals": ["x"]})], name="s").invoke( + empty_state(), config={"configurable": {"thread_id": "from-thread"}} + ) + assert {r["session_id"] for r in read_events(tmp_path)} == {"from-thread"} + + +def test_session_id_falls_back_to_the_root_run_id(tmp_path, instrumented): + build_simple([("n", lambda s: {"vals": ["x"]})], name="s").invoke(empty_state()) + sessions = {r["session_id"] for r in read_events(tmp_path)} + assert len(sessions) == 1 + # A run id, not a synthesised one: a made-up session splits one run in two. + assert _looks_like_a_uuid(next(iter(sessions))) + + +def test_an_ambient_agent_scope_and_the_adapter_produce_one_tree(tmp_path, instrumented): + with failproofai_sdk.agent("planner", goal="do the thing"): + build_simple([("n", lambda s: {"vals": ["x"]})], name="inner").invoke( + empty_state(), config={"configurable": {"thread_id": "ignored"}} + ) + rows = read_events(tmp_path) + assert len({r["session_id"] for r in rows}) == 1 + starts = only(rows, "agent_start") + assert [r["agent_id"] for r in starts] == ["planner", "inner"] + # This is the whole interop story: a hand-written outer bracket and an + # adapter must produce one tree, not two disconnected sessions. + assert starts[1]["parent_id"] == "planner" + + +def test_an_explicit_session_id_option_wins(tmp_path): + failproofai_sdk.instrument("langchain", session_id="forced") + try: + build_simple([("n", lambda s: {"vals": ["x"]})], name="s").invoke( + empty_state(), config={"configurable": {"thread_id": "ignored"}} + ) + finally: + failproofai_sdk.uninstrument("langchain") + assert {r["session_id"] for r in read_events(tmp_path)} == {"forced"} + + +# --------------------------------------------------------------------------- +# Install / uninstall +# --------------------------------------------------------------------------- + +def test_uninstrument_stops_recording_and_is_idempotent(tmp_path): + failproofai_sdk.instrument("langchain") + app = build_simple([("n", lambda s: {"vals": ["x"]})], name="s") + app.invoke(empty_state(), config={"configurable": {"thread_id": "on"}}) + before = len(read_events(tmp_path)) + assert before + + assert failproofai_sdk.uninstrument("langchain") == ("langchain",) + assert failproofai_sdk.uninstrument("langchain") == () + assert adapter.ENV_VAR not in os.environ + + app.invoke(empty_state(), config={"configurable": {"thread_id": "off"}}) + assert len(read_events(tmp_path)) == before + + +def test_instrumenting_twice_does_not_double_record(tmp_path): + failproofai_sdk.instrument("langchain") + assert failproofai_sdk.instrument("langchain") == () + try: + build_simple([("n", lambda s: {"vals": ["x"]})], name="s").invoke( + empty_state(), config={"configurable": {"thread_id": "twice"}} + ) + finally: + failproofai_sdk.uninstrument("langchain") + rows = read_events(tmp_path) + assert len(only(rows, "agent_start")) == 1 + assert len(only(rows, "hook_triggered")) == 1 + + +def test_autodetect_picks_up_langchain(tmp_path): + installed = failproofai_sdk.instrument() + try: + assert "langchain" in installed + finally: + failproofai_sdk.uninstrument() + + +def test_open_leaves_are_closed_when_the_root_run_ends(tmp_path, instrumented): + """`agent_end` force-closes open pauses but not tools, models or humans.""" + from failproofai_sdk.integrations.langchain import _RunInfo, _STATE + + def leaky(state): + # Simulate a framework that never delivered the end callback: register a + # tool run by hand and leave it open. + with _STATE.lock: + root = next(i for i in _STATE.runs.values() if i.kind == "root") + orphan = _RunInfo( + id="orphan", parent=root.id, name="ghost", run_type="tool", kind="tool" + ) + orphan.root = root.id + orphan.session = root.session + orphan.tool_call_id = "orphan" + _STATE.runs["orphan"] = orphan + _STATE.tracker.link("orphan", root.id) + _STATE.tracker.emit( + "tool_use", "orphan", parent_key=root.id, tool_name="ghost", tool_call_id="orphan" + ) + return {"vals": ["x"]} + + build_simple([("leaky", leaky)], name="leaky").invoke( + empty_state(), config={"configurable": {"thread_id": "leak"}} + ) + rows = read_events(tmp_path) + ghosts = [r for r in only(rows, "tool_result") if r["tool_name"] == "ghost"] + assert len(ghosts) == 1 + assert ghosts[0]["fw_incomplete"] is True + assert rows[-1]["type"] == "agent_end" + + +# --------------------------------------------------------------------------- +# Async +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_ainvoke_produces_the_same_shape(tmp_path, instrumented): + model = fake_model(tool_calling_message()) + + async def plan(state, config: RunnableConfig): + return { + "messages": [await model.ainvoke(state["messages"], config=config)], + "vals": ["p"], + } + + async def act(state, config: RunnableConfig): + return {"vals": [await adder.ainvoke({"a": 1, "b": 2}, config=config)]} + + app = build_simple([("plan", plan), ("act", act)], name="async_pipeline") + await app.ainvoke(empty_state(), config={"configurable": {"thread_id": "async"}}) + + assert types_of(read_events(tmp_path)) == [ + "agent_start", + "hook_triggered", + "model_request", + "model_response", + "hook_completed", + "hook_triggered", + "tool_use", + "tool_result", + "hook_completed", + "agent_end", + ] + + +# --------------------------------------------------------------------------- +# Structural anti-drift — the highest-value tests in this file +# --------------------------------------------------------------------------- + +def test_every_override_still_exists_on_its_framework_base(): + """Reflect over the handler and check it still overrides something real. + + This is the test that catches the failure mode nothing else can. If + LangChain renames `on_tool_error` or drops `_start_trace`, our method stops + being called, we emit nothing for that path, and every behavioural test in + this file still passes — they assert on the events we *did* emit, and a + silent gap looks exactly like a run that had no tool errors. The in-repo + precedent is `cli/agenteye_cli/_click_compat.py`, where an `isinstance` + check went quietly always-False while its own anti-drift test stayed green. + """ + cls = adapter.FailproofAITracer + overrides = { + name: fn + for name, fn in vars(cls).items() + if inspect.isfunction(fn) and not name.startswith("__") + } + assert len(overrides) >= 10, "reflection found nothing — the test would be vacuous" + + for name, fn in sorted(overrides.items()): + base = next((b for b in cls.__mro__[1:] if name in vars(b)), None) + assert base is not None, ( + "%s overrides nothing on any base — it is dead code that the " + "framework will never call" % name + ) + base_fn = vars(base)[name] + base_params = inspect.signature(base_fn).parameters + for param_name, param in inspect.signature(fn).parameters.items(): + if param.kind in (param.VAR_KEYWORD, param.VAR_POSITIONAL): + continue + # Deliberately no "...or the base takes **kwargs" escape hatch: a + # renamed parameter would still be swallowed by the base's **kwargs + # and our own parameter would silently never be filled. + assert param_name in base_params, ( + "%s.%s declares %r, which no longer appears in %s.%s%s" + % (cls.__name__, name, param_name, base.__name__, name, + inspect.signature(base_fn)) + ) + + +def test_the_graph_lifecycle_overrides_are_bound_to_the_real_langgraph_base(): + """`on_interrupt` lives on a stand-in when langgraph is too old. + + If the import silently fell back on a machine that *has* langgraph 1.2, + langgraph's `isinstance(h, GraphCallbackHandler)` filter would never match + us and HITL would go dark with nothing to show for it. + """ + from langgraph.callbacks import GraphCallbackHandler + + assert adapter._GraphCallbackHandler is GraphCallbackHandler + assert issubclass(adapter.FailproofAITracer, GraphCallbackHandler) + for name in ("on_interrupt", "on_resume"): + assert name in vars(GraphCallbackHandler) + + +def test_the_handler_flags_the_callback_manager_depends_on(): + cls = adapter.FailproofAITracer + # AsyncCallbackManager dispatches sync handlers through run_in_executor + # unless run_inline, and that hop can REORDER callbacks — which scrambles + # timestamp order and breaks every pairing in this file. + assert cls.run_inline is True + + # raise_error follows FAILPROOFAI_SDK_STRICT rather than being a constant, so it + # has to be read off an INSTANCE -- which is also how LangChain reads it. + # Normally False: LangChain already firewalls handler exceptions, and + # raising would take the customer's graph down with our bug. Under strict + # it must be True, or LangChain's firewall swallows the exception `safe()` + # re-raises and the escape hatch does nothing at all here. + handler = cls() + _core.set_strict(False) + assert handler.raise_error is False + _core.set_strict(True) + assert handler.raise_error is True + _core.set_strict(None) + + from langchain_core.callbacks.base import BaseCallbackHandler + + assert hasattr(BaseCallbackHandler, "run_inline") + assert hasattr(BaseCallbackHandler, "raise_error") + + +def test_the_configure_hook_api_is_still_the_one_we_build_on(): + from langchain_core.tracers import context + + assert callable(context.register_configure_hook) + params = list(inspect.signature(context.register_configure_hook).parameters) + assert params[:4] == ["context_var", "inheritable", "handle_class", "env_var"] + # There is deliberately no deregister API — uninstall() has to work by + # emptying the ContextVar and the env var instead. If one ever appears, this + # assertion is the reminder to use it. + assert not hasattr(context, "unregister_configure_hook") + + +def test_the_control_flow_exception_hierarchy_is_still_where_we_look(): + from langgraph.errors import GraphBubbleUp, GraphInterrupt, ParentCommand + + assert adapter._GraphBubbleUp is GraphBubbleUp + assert issubclass(GraphInterrupt, GraphBubbleUp) + assert issubclass(ParentCommand, GraphBubbleUp) + # ...and the payload shape `_interrupts_of` reads. + from langgraph.types import Interrupt + + interrupt_obj = Interrupt(value={"prompt": "p"}, id="abc") + assert adapter._interrupts_of(GraphInterrupt([interrupt_obj])) == (interrupt_obj,) + # A ParentCommand is control flow but carries a Command, not interrupts, so + # it must not be mistaken for a human pause. + assert adapter._is_control_flow(ParentCommand(Command(resume=1))) + assert adapter._interrupts_of(ParentCommand(Command(resume=1))) == () + assert not adapter._is_control_flow(ValueError("real")) + + +def test_the_lifecycle_event_dataclasses_still_carry_the_fields_we_read(): + import dataclasses + + from langgraph.callbacks import GraphInterruptEvent, GraphResumeEvent + + interrupt_fields = {f.name for f in dataclasses.fields(GraphInterruptEvent)} + assert {"run_id", "interrupts"} <= interrupt_fields + assert "run_id" in {f.name for f in dataclasses.fields(GraphResumeEvent)} + + from langgraph.types import Interrupt + + assert {"value", "id"} <= {f.name for f in dataclasses.fields(Interrupt)} + + +def test_the_node_filter_still_matches_real_langgraph_metadata(tmp_path): + """The node filter is a string comparison. Prove the strings still exist. + + `metadata["langgraph_node"] == run.name` is what separates a node from every + inner Runnable that inherits the same metadata. If the key is renamed, every + node stops being a hook and the only symptom is a shorter timeline. + """ + from langchain_core.tracers.base import BaseTracer + + seen = [] + + class Probe(BaseTracer): + run_inline = True + + def _persist_run(self, run): + pass + + def _start_trace(self, run): + super()._start_trace(run) + seen.append((run.name, dict(run.metadata or {}), run.run_type)) + + graph = StateGraph(State) + graph.add_node("only_node", lambda s: {"vals": ["x"]}) + graph.add_edge(START, "only_node") + graph.add_edge("only_node", END) + graph.compile(name="probe").invoke( + empty_state(), config={"callbacks": [Probe()], "configurable": {"thread_id": "t"}} + ) + + root_name, root_meta, _ = seen[0] + node_name, node_meta, _ = seen[1] + assert root_meta.get("langgraph_node") is None + assert node_meta["langgraph_node"] == node_name == "only_node" + assert "langgraph_checkpoint_ns" in node_meta + assert node_meta["thread_id"] == "t" + # And a subgraph node's checkpoint namespace is still `|`-separated, which + # is how nested agents are derived without recognising a Pregel object. + seen.clear() + outer = StateGraph(State) + outer.add_node("child", subgraph()) + outer.add_edge(START, "child") + outer.add_edge("child", END) + outer.compile(name="outer").invoke( + empty_state(), config={"callbacks": [Probe()], "configurable": {"thread_id": "t2"}} + ) + inner = [m for name, m, _ in seen if m.get("langgraph_node") == name == "sub_step"] + assert inner and "|" in inner[0]["langgraph_checkpoint_ns"] + + +def test_the_tool_call_id_still_reaches_the_run_object(): + """`tool_call_id` is read off `run.extra`, where `_create_tool_run` parks + the callback kwargs. If that stops happening we would silently fall back to + the run id and every tool would stop lining up with the provider's logs.""" + from langchain_core.tracers.base import BaseTracer + + seen = [] + + class Probe(BaseTracer): + run_inline = True + + def _persist_run(self, run): + pass + + def _start_trace(self, run): + super()._start_trace(run) + if run.run_type == "tool": + seen.append(dict(run.extra or {})) + + model = fake_model(tool_calling_message()) + + def plan(state): + return {"messages": [model.invoke(state["messages"])], "vals": ["p"]} + + graph = StateGraph(State) + graph.add_node("plan", plan) + graph.add_node("tools", ToolNode([adder])) + graph.add_edge(START, "plan") + graph.add_edge("plan", "tools") + graph.add_edge("tools", END) + graph.compile(name="ids").invoke( + empty_state(), config={"callbacks": [Probe()], "configurable": {"thread_id": "t"}} + ) + assert seen and seen[0].get("tool_call_id") == "call_abc" + + +def test_usage_metadata_survives_to_on_llm_end(): + """The primary token source. The two fallbacks exist because providers + disagree; this asserts the primary is still the primary.""" + from langchain_core.tracers.base import BaseTracer + + seen = [] + + class Probe(BaseTracer): + run_inline = True + + def _persist_run(self, run): + pass + + def on_llm_end(self, response, *, run_id, **kwargs): + seen.append(adapter._usage(response)) + return super().on_llm_end(response, run_id=run_id, **kwargs) + + model = fake_model(tool_calling_message()) + model.invoke([HumanMessage("hi")], config={"callbacks": [Probe()]}) + assert seen == [{"input_tokens": 11, "output_tokens": 5, "total_tokens": 16}] + + +# --------------------------------------------------------------------------- +# A root run that is itself a leaf +# --------------------------------------------------------------------------- +# +# `ChatOpenAI(...).invoke(...)` outside any graph arrives as ONE run with no +# parent and `run_type="chat_model"`. Handled only as a root it produced +# `agent_start`/`agent_end` and nothing else: no `model_request`, no +# `model_response`, so the model name, both token counts and the latency of a +# direct model call were dropped while the trace still looked populated. +# +# Direct `.invoke()` is not an edge case — a classifier, a summariser and a +# one-shot rewrite are all shaped exactly like this. + + +def test_a_bare_model_call_still_emits_its_model_pair(tmp_path, instrumented): + with failproofai_sdk.session(): + fake_model(AIMessage("hi", usage_metadata={ + "input_tokens": 7, "output_tokens": 3, "total_tokens": 10, + })).invoke("say hi") + + rows = read_events(tmp_path) + kinds = types_of(rows) + assert "model_request" in kinds, ( + f"a bare model call emitted {kinds} — the model pair is missing, so the " + f"model name, token counts and latency of every direct .invoke() are lost" + ) + assert "model_response" in kinds + + +def test_a_bare_model_call_records_tokens_and_an_int_duration(tmp_path, instrumented): + with failproofai_sdk.session(): + fake_model(AIMessage("hi", usage_metadata={ + "input_tokens": 7, "output_tokens": 3, "total_tokens": 10, + })).invoke("say hi") + + response = only(read_events(tmp_path), "model_response")[0] + assert response["input_tokens"] == 7 + assert response["output_tokens"] == 3 + # u32 column: a float silently NULLs it server-side. + assert isinstance(response["duration_ms"], int) + assert response["model"] + + +def test_a_bare_model_calls_pair_sits_inside_its_agent_span(tmp_path, instrumented): + """Order matters: the dashboard closes the agent span at `agent_end`, so a + `model_response` after it is attributed to nothing.""" + with failproofai_sdk.session(): + fake_model(AIMessage("hi")).invoke("say hi") + + kinds = types_of(read_events(tmp_path)) + assert kinds.index("agent_start") < kinds.index("model_request") + assert kinds.index("model_response") < kinds.index("agent_end") + + +def test_a_bare_model_pair_shares_one_request_id(tmp_path, instrumented): + with failproofai_sdk.session(): + fake_model(AIMessage("hi")).invoke("say hi") + + rows = read_events(tmp_path) + request = only(rows, "model_request")[0] + response = only(rows, "model_response")[0] + assert request["request_id"] == response["request_id"] + + +def test_a_graph_run_is_not_treated_as_a_leaf(tmp_path, instrumented): + """The fix is additive and must not fire for a chain-typed root.""" + build_simple([("only", lambda state: {"vals": ["x"]})]).invoke(empty_state()) + rows = read_events(tmp_path) + # A graph root emits no model pair of its own — only its nodes do. + assert types_of(rows).count("agent_start") == 1 + + +# --------------------------------------------------------------------------- +# Two roots that merely OVERLAP are not a resume +# --------------------------------------------------------------------------- +# +# `_start_root` reuses an existing session's agent when that agent is still +# open, because that is what an interrupt/resume looks like: the paused +# `.invoke()` deliberately did not close its agent and the resuming one must not +# open a second root span for the same logical run. +# +# "Still open" is ALSO true of two roots that merely overlap in time under one +# session id, and that is not exotic — langchain-core opens one root run **per +# input** for `.batch()`, and any two requests carrying the same conversation id +# through `SESSION_METADATA_KEY` do the same. Read as a resume, the second root +# got no `agent_start` at all, its work was relabelled with the first root's +# `agent_id`, the first root to finish closed the shared agent, and everything +# the other root emitted afterwards resolved to nothing and was DROPPED. +# +# The discriminator is `open_pauses`: `_end_root` skips `agent_end` exactly when +# it is non-empty, which is the only way an agent outlives its root, and +# `_suspend` is the only thing that fills it. + + +def test_two_overlapping_roots_in_one_session_are_two_agents(tmp_path, instrumented): + import threading + + # The barrier is the whole point: both roots are guaranteed to be OPEN at + # the same time, which is the state that used to be misread as a resume. + # Without it this races and passes against the bug about half the time. + barrier = threading.Barrier(2, timeout=10) + + def hold(state): + barrier.wait() + return {"vals": ["x"]} + + app = build_simple([("n", hold)], name="overlap") + + def run(): + app.invoke( + empty_state(), + config={"metadata": {adapter.SESSION_METADATA_KEY: "one-session"}}, + ) + + threads = [threading.Thread(target=run) for _ in range(2)] + for thread in threads: + thread.start() + for thread in threads: + thread.join(timeout=30) + + rows = read_events(tmp_path) + assert {r["session_id"] for r in rows} == {"one-session"} + starts = only(rows, "agent_start") + ends = only(rows, "agent_end") + assert len(starts) == 2, ( + f"two overlapping roots produced {len(starts)} agent_start(s): the second " + f"root was read as a resume of the first" + ) + assert len(ends) == 2 + # Distinct runs, not one run reported twice. + assert len({r["fw_run_id"] for r in starts}) == 2 + # Nothing was dropped on the way: each root ran the node once. + assert types_of(rows).count("hook_triggered") == 2 + assert types_of(rows).count("hook_completed") == 2 + # `sink` (autouse) fails this test on the "could not resolve a session for + # run ... and is dropping its events" warning the old behaviour produced, + # which is the other half of the regression and the half that was silent. + + +def test_a_genuine_interrupt_resume_is_still_one_agent_not_two(tmp_path, instrumented): + """The counterweight: `open_pauses` must not disable the resume path. + + Deleting the resume branch would also "fix" the overlap bug above, at the + cost of splitting every human approval into two root spans and zeroing the + `agent_pause` -> `agent_resume` interval that is the only measure of how + long the human took. + """ + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "resume-one"}} + app.invoke(empty_state(), config=config) + app.invoke(Command(resume="yes"), config=config) + + rows = read_events(tmp_path) + roots = [r for r in only(rows, "agent_start") if not r.get("parent_id")] + assert len(roots) == 1, "the resuming .invoke() opened a second root span" + assert len([r for r in only(rows, "agent_end") if r["agent_id"] == "root_graph"]) == 1 + assert types_of(only(rows, "agent_pause", "agent_resume")) == [ + "agent_pause", + "agent_resume", + ] + + +# --------------------------------------------------------------------------- +# A failing root-run-that-is-a-leaf owns its failure exactly once +# --------------------------------------------------------------------------- +# +# `_on_end` returns straight after `_end_root` for a root, so the line at the +# bottom of the function that marks the failure as owned by the span below never +# ran for a root that was ALSO a leaf. The same exception was then reported +# twice — once as `tool_result.error` / `model_response.error` and again as a +# standalone `error` event — and the server derives `is_error` from both, so one +# failure counted as two on `sessionSummary.errorCount`. The identical failure +# one Runnable deeper counted as one. + + +def test_a_failing_bare_tool_reports_its_error_once(tmp_path, instrumented): + with failproofai_sdk.session(): + with pytest.raises(RuntimeError, match="tool boom"): + exploder.invoke({"x": 1}) + + rows = read_events(tmp_path) + result = only(rows, "tool_result")[0] + assert "tool boom" in result["error"] + # The span that owns the failure has reported it; a standalone `error` event + # on top is the same failure counted twice. + assert not only(rows, "error"), ( + "a top-level tool failure was reported both on tool_result and as a " + "standalone error event" + ) + assert only(rows, "agent_end")[0]["outcome"] == "failed" + + +def test_a_failing_bare_model_call_reports_its_error_once(tmp_path, instrumented): + class _BoomModel(GenericFakeChatModel): + def _generate(self, *args, **kwargs): + raise RuntimeError("model boom") + + with failproofai_sdk.session(): + with pytest.raises(RuntimeError, match="model boom"): + _BoomModel(messages=iter([])).invoke("say hi") + + rows = read_events(tmp_path) + response = only(rows, "model_response")[0] + assert "model boom" in response["error"] + assert response["stop_reason"] == "error" + assert not only(rows, "error") + assert only(rows, "agent_end")[0]["outcome"] == "failed" + + +def test_a_failure_below_the_root_still_produces_its_one_error_event( + tmp_path, instrumented +): + """The other side of the same line: a root nothing below reported must still + get exactly one standalone `error`, or the failure reaches no surface.""" + + def boom(_payload): + raise RuntimeError("chain boom") + + with failproofai_sdk.session(): + with pytest.raises(RuntimeError, match="chain boom"): + RunnableLambda(boom).with_config(run_name="boomer").invoke({"x": 1}) + + rows = read_events(tmp_path) + assert len(only(rows, "error")) == 1 + assert only(rows, "agent_end")[0]["outcome"] == "failed" + + +# --------------------------------------------------------------------------- +# `tool_result.output` is the tool's result, not a repr of the envelope +# --------------------------------------------------------------------------- +# +# A tool handed the LLM's `ToolCall` dict — what `bind_tools` produces and what +# every modern tool loop passes — returns a `ToolMessage`. `truncate` has no +# JSON shape for one, so the single most-read field in a tool loop rendered as +# `ToolMessage(content='3', name='adder', tool_call_id='call_zz', ...)`. +# +# `status` is the second half: a `ToolMessage` carries `status="error"` when the +# tool failed but the framework turned the exception into a message for the +# model instead of raising. `run.error` is empty on that path, so the failure +# had no representation at all — `is_error` 0, a green span, and the text of the +# failure sitting in an output field nobody filters on. + + +def test_a_tool_called_with_a_tool_call_records_its_content_not_a_repr( + tmp_path, instrumented +): + with failproofai_sdk.session(): + adder.invoke( + {"name": "adder", "args": {"a": 1, "b": 2}, "id": "call_zz", "type": "tool_call"} + ) + + result = only(read_events(tmp_path), "tool_result")[0] + assert result["output"] == "3", ( + f"tool_result.output is {result['output']!r} — the ToolMessage envelope " + f"leaked instead of the tool's own result" + ) + assert "ToolMessage(" not in str(result["output"]) + assert result["tool_call_id"] == "call_zz" + + +def test_a_tool_that_fails_without_raising_is_still_an_error(tmp_path, instrumented): + @tool + def quiet_failer(x: int) -> str: + """Fails without raising: returns an error-status ToolMessage.""" + return ToolMessage(content="upstream 503", tool_call_id="unused", status="error") + + with failproofai_sdk.session(): + quiet_failer.invoke( + {"name": "quiet_failer", "args": {"x": 1}, "id": "call_q", "type": "tool_call"} + ) + + result = only(read_events(tmp_path), "tool_result")[0] + assert result.get("error"), ( + "a tool that reported failure through ToolMessage(status='error') was " + "recorded as a success" + ) + assert "upstream 503" in result["error"] + + +def test_a_successful_tool_message_carries_no_error(tmp_path, instrumented): + """The `status` read must not turn every ToolMessage into a failure.""" + with failproofai_sdk.session(): + adder.invoke( + {"name": "adder", "args": {"a": 1, "b": 2}, "id": "call_ok", "type": "tool_call"} + ) + assert only(read_events(tmp_path), "tool_result")[0].get("error") is None + + +# --------------------------------------------------------------------------- +# The `error` event does not repeat its own type +# --------------------------------------------------------------------------- +# +# `error` is the one event that carries `error_type` as its OWN field, and the +# server builds the row's `summary` as ": ". Feeding it +# `_error_text` — which prefixes the type because `tool_result.error` and +# `agent_end.summary` have nowhere else to say it — rendered every entry on the +# Errors surface as `ValueError: ValueError: denominator must be non-zero`. +# CrewAI, LlamaIndex and Pydantic AI all pass a bare `str(exc)` here. + + +def test_the_error_events_message_does_not_repeat_its_own_type(tmp_path, instrumented): + def boom(_payload): + raise RuntimeError("chain boom") + + with failproofai_sdk.session(): + with pytest.raises(RuntimeError, match="chain boom"): + RunnableLambda(boom).with_config(run_name="boomer").invoke({"x": 1}) + + rows = read_events(tmp_path) + event = only(rows, "error")[0] + assert event["error_type"] == "RuntimeError" + assert event["message"] == "chain boom", ( + f"message is {event['message']!r} — the server renders summary as " + f"': ', so a prefixed message says it twice" + ) + # Scoped to the `error` event: `agent_end.summary` has no `error_type` + # field beside it, so it keeps naming the exception type itself. + assert only(rows, "agent_end")[0]["summary"] == "RuntimeError: chain boom" + + +# --------------------------------------------------------------------------- +# `uninstrument()` when the trace env var was exported by somebody else +# --------------------------------------------------------------------------- +# +# A configure hook cannot be deregistered, so removal is "make the hook produce +# nothing" — and neither of the two levers `uninstall()` had actually does that +# in every process. Clearing `_HANDLER_VAR` only reaches contexts derived from +# the caller's, and the env var is unset only when `install()` was the one that +# set it (it must not clobber somebody else's environment). Exported by a +# Dockerfile or a CI job, it left `_configure` constructing a live zero-arg +# tracer per callback manager, and a fully torn-down adapter went on recording +# every event forever. + + +def test_uninstrument_stops_recording_when_the_env_var_was_already_set( + tmp_path, monkeypatch +): + monkeypatch.setenv(adapter.ENV_VAR, "1") + failproofai_sdk.instrument("langchain") + app = build_simple([("n", lambda s: {"vals": ["x"]})], name="s") + app.invoke(empty_state(), config={"configurable": {"thread_id": "on"}}) + before = len(read_events(tmp_path)) + assert before + + assert failproofai_sdk.uninstrument("langchain") == ("langchain",) + # Deliberately still set: `install()` did not set it, so `uninstall()` does + # not get to remove it. That is exactly why it cannot be the kill switch. + assert os.environ.get(adapter.ENV_VAR) == "1" + + app.invoke(empty_state(), config={"configurable": {"thread_id": "off"}}) + assert len(read_events(tmp_path)) == before, ( + "the adapter kept recording after uninstrument() because the trace env " + "var was set before instrument() ran" + ) + + +def test_reinstrumenting_after_that_teardown_records_again(tmp_path, monkeypatch): + """The kill switch must be a switch, not a one-way fuse.""" + monkeypatch.setenv(adapter.ENV_VAR, "1") + failproofai_sdk.instrument("langchain") + failproofai_sdk.uninstrument("langchain") + failproofai_sdk.instrument("langchain") + try: + build_simple([("n", lambda s: {"vals": ["x"]})], name="s").invoke( + empty_state(), config={"configurable": {"thread_id": "again"}} + ) + finally: + failproofai_sdk.uninstrument("langchain") + assert types_of(read_events(tmp_path)).count("agent_start") == 1 + + +# --------------------------------------------------------------------------- +# The node key and the run name are BOTH the user's to choose +# --------------------------------------------------------------------------- +# +# `_node_of` matched on `run.name == metadata["langgraph_node"]` alone, and both +# sides of that comparison are strings a user picks. Every inner run of a node +# inherits `langgraph_node`, so the moment an inner run happens to carry the +# node's name it was recorded as a second visit to the node instead of as what +# it is — and the event that was actually worth having never got emitted. +# +# Verified against langgraph 1.2.11: whatever you hand `add_node`, the node's +# OWN run is always a `chain` run tagged `graph:step:N`, and the thing you +# passed runs beneath it tagged `seq:step:N`. Those two facts are the fix. + + +def test_a_node_named_after_its_tool_still_records_the_tool(tmp_path, instrumented): + """`add_node("adder", ToolNode([adder]))` — the obvious naming — used to + delete the tool call: no `tool_use`, no `tool_result`, no `tool_call_id`, + and `/tools` showing the call had never happened.""" + + def plan(state): + return {"messages": [tool_calling_message()], "vals": ["plan"]} + + graph = StateGraph(State) + graph.add_node("plan", plan) + graph.add_node("adder", ToolNode([adder])) # node key == tool name + graph.add_edge(START, "plan") + graph.add_edge("plan", "adder") + graph.add_edge("adder", END) + graph.compile(name="collide").invoke( + empty_state(), config={"configurable": {"thread_id": "tool-collision"}} + ) + + rows = read_events(tmp_path) + tools = only(rows, "tool_use", "tool_result") + assert types_of(tools) == ["tool_use", "tool_result"], ( + "the tool run was misfiled as a second visit to the node of the same name" + ) + assert tools[0]["tool_name"] == tools[1]["tool_name"] == "adder" + assert tools[0]["tool_call_id"] == tools[1]["tool_call_id"] == "call_abc" + assert tools[1]["output"] == "3" + # ...and the node itself is still exactly one hook, not two. + assert [r["hook_name"] for r in only(rows, "hook_triggered")] == ["plan", "adder"] + + +def test_a_node_named_after_its_model_still_records_the_model(tmp_path, instrumented): + """Same collision one run type over: the model name, both token counts and + the latency were dropped while the trace still looked populated.""" + model = fake_model(tool_calling_message()) + + def call(state): + return {"messages": [model.invoke(state["messages"])], "vals": ["x"]} + + # `GenericFakeChatModel`'s run name is its class name. + app = build_simple([("GenericFakeChatModel", call)], name="model-collide") + app.invoke(empty_state(), config={"configurable": {"thread_id": "model-collision"}}) + + rows = read_events(tmp_path) + assert types_of(only(rows, "model_request", "model_response")) == [ + "model_request", + "model_response", + ], "the chat model run was misfiled as a second visit to the node of the same name" + response = only(rows, "model_response")[0] + assert response["input_tokens"] == 11 + assert response["output_tokens"] == 5 + assert isinstance(response["duration_ms"], int) + assert len(only(rows, "hook_triggered")) == 1 + + +def test_an_inner_runnable_sharing_the_node_name_is_not_a_second_visit( + tmp_path, instrumented +): + """`add_node("same", something.with_config(run_name="same"))` produced TWO + `hook_triggered`/`hook_completed` pairs for one visit, doubling that node's + count on `/hooks` and halving its apparent latency.""" + inner = RunnableLambda(lambda state: {"vals": ["same"]}).with_config(run_name="same") + app = build_simple([("same", inner)], name="dup") + app.invoke(empty_state(), config={"configurable": {"thread_id": "dup"}}) + + rows = read_events(tmp_path) + assert types_of(only(rows, "hook_triggered", "hook_completed")) == [ + "hook_triggered", + "hook_completed", + ] + + +def test_the_node_exclusions_do_not_swallow_real_nodes(tmp_path, instrumented): + """The counterweight. Both new conditions are exclusions, and `_node_of` + gates `hook_triggered` AND `_ensure_subgraph_agent` — over-tighten it and + the timeline loses every node and every subgraph agent at once.""" + app = build_graph() + app.invoke(empty_state(), config={"configurable": {"thread_id": "counterweight"}}) + + rows = read_events(tmp_path) + names = [r["hook_name"] for r in only(rows, "hook_triggered")] + # A plain function node, a ToolNode, a compiled subgraph as a node, two + # Send-dispatched copies of one node: all still hooks. + assert names.count("plan") == 1 + assert names.count("tools") == 1 + assert names.count("child") == 1 + assert names.count("worker") == 2 + assert names.count("sub_step") == 1 + # And the subgraph is still a nested agent, which only happens from inside + # the node branch of `_on_start`. + assert [r["agent_id"] for r in only(rows, "agent_start")] == [ + "root_graph", + "root_graph/child", + ] + + +# --------------------------------------------------------------------------- +# A run that merely OVERLAPS a pause is not the approval +# --------------------------------------------------------------------------- + + +def test_an_unrelated_run_during_a_pause_is_not_read_as_the_approval( + tmp_path, instrumented +): + """A HITL turn sits paused on a human for minutes. Any other run carrying + the same session id in that window — a second request on one conversation + id, a background summariser, a different graph entirely — was read as the + answer: it got no `agent_start` of its own, its work was folded into the + paused span, and the adapter emitted `agent_resume` + `human_input` for a + human who had answered nothing. Fabricating an approval is the worst wrong + answer a human-approval product can give.""" + session = {"metadata": {adapter.SESSION_METADATA_KEY: "one-conversation"}} + paused = build_graph(checkpointer=InMemorySaver()) + paused.invoke( + empty_state(), config={"configurable": {"thread_id": "held"}, **session} + ) + assert types_of(only(read_events(tmp_path), "agent_pause")) == ["agent_pause"] + + unrelated = build_simple([("summarise", lambda s: {"vals": ["s"]})], name="other") + unrelated.invoke(empty_state(), config=dict(session)) + + rows = read_events(tmp_path) + assert not only(rows, "agent_resume"), "an unrelated run closed the human's pause" + assert not only(rows, "human_input"), "an approval was recorded that never happened" + # The unrelated run is its own agent, with its own span, not a relabelled + # continuation of the paused one. + assert "other" in [r["agent_id"] for r in only(rows, "agent_start")] + assert "other" in [r["agent_id"] for r in only(rows, "agent_end")] + + +def test_a_none_input_is_still_a_continuation_of_the_pause(tmp_path, instrumented): + """The counterweight for `_is_continuation`: `invoke(None, config)` is + langgraph's other documented way to resume, and narrowing the test to + `Command` alone would split that run in two.""" + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "none-resume"}} + app.invoke(empty_state(), config=config) + app.invoke(None, config=config) + + rows = read_events(tmp_path) + roots = [r for r in only(rows, "agent_start") if not r.get("parent_id")] + assert len(roots) == 1, "invoke(None) opened a second root span" + # No answer was supplied, so the node interrupts again and the pause + # reopens — but the FIRST one was closed, on the same span. + assert types_of(only(rows, "agent_pause", "agent_resume"))[:3] == [ + "agent_pause", + "agent_resume", + "agent_pause", + ] + + +# --------------------------------------------------------------------------- +# The resume arrives in a DIFFERENT PROCESS +# --------------------------------------------------------------------------- +# +# Every pause above is keyed on the `Interrupt` object the pausing process saw, +# which assumes the process that paused is the process that resumes. Real HITL +# is not shaped like that: one worker serves the request that interrupts, a +# human answers minutes later, and whichever worker picks that request up +# resumes against the shared checkpointer. `_STATE` is per process, so the +# resuming worker had no `_Session` and no `open_pauses` — it emitted no +# `agent_resume` and no `human_input` at all, and the `human_wait` / +# `agent_pause` from the first worker stayed open FOREVER. Every cross-process +# approval left its session reporting "still waiting on a human" after the human +# had answered. +# +# `_STATE.reset()` between the two `.invoke()` calls is exactly a fresh process: +# it is the same clear the adapter does at `install()`, and the events the first +# "process" wrote are already on disk. + + +def _forget_everything_this_process_knows(): + adapter._STATE.reset() + + +def test_a_resume_from_another_process_still_closes_the_pause(tmp_path, instrumented): + saver = InMemorySaver() + config = {"configurable": {"thread_id": "xproc"}} + + build_graph(checkpointer=saver).invoke(empty_state(), config=config) + opened = only(read_events(tmp_path), "human_wait") + assert len(opened) == 1 + + _forget_everything_this_process_knows() + build_graph(checkpointer=saver).invoke(Command(resume="approved"), config=config) + + rows = read_events(tmp_path) + resumes = only(rows, "agent_resume") + answers = only(rows, "human_input") + assert len(resumes) == 1 and len(answers) == 1, ( + "the pause opened by the other process was never closed" + ) + # Correlated on the id the FIRST process reported, with no shared state: + # langgraph derives `Interrupt.id` from the interrupted task's checkpoint + # namespace, which is byte-identical across the two invocations. + assert resumes[0]["pause_id"] == opened[0]["input_id"] + assert answers[0]["input_id"] == opened[0]["input_id"] + assert answers[0]["response"] == "approved" + + +def test_a_remote_resume_credits_the_interrupted_node_not_the_subgraph_host( + tmp_path, instrumented +): + """The subgraph host node re-runs too, at a shallower namespace, and it + starts *before* the lifecycle event that unmasks it. Crediting the pause to + it would emit an id that correlates with nothing and leave the real pause + open — the exact failure being fixed, wearing a fix.""" + saver = InMemorySaver() + config = {"configurable": {"thread_id": "xproc-sub"}} + + def build(): + inner = StateGraph(State) + inner.add_node("sub_pre", lambda state: {"vals": ["pre"]}) + inner.add_node("ask", lambda state: {"answer": str(interrupt({"prompt": "ok?"}))}) + inner.add_edge(START, "sub_pre") + inner.add_edge("sub_pre", "ask") + inner.add_edge("ask", END) + outer = StateGraph(State) + outer.add_node("before", lambda state: {"vals": ["b"]}) + outer.add_node("child", inner.compile(name="child_graph")) + outer.add_node("after", lambda state: {"vals": ["a"]}) + outer.add_edge(START, "before") + outer.add_edge("before", "child") + outer.add_edge("child", "after") + outer.add_edge("after", END) + return outer.compile(name="root_graph", checkpointer=saver) + + build().invoke(empty_state(), config=config) + opened = only(read_events(tmp_path), "human_wait") + assert len(opened) == 1 + + _forget_everything_this_process_knows() + build().invoke(Command(resume="yes"), config=config) + + rows = read_events(tmp_path) + # Exactly one pair — not one per node that re-ran, and not one for the + # subgraph host. + assert len(only(rows, "agent_resume")) == 1 + assert len(only(rows, "human_input")) == 1 + assert only(rows, "agent_resume")[0]["pause_id"] == opened[0]["input_id"] + + +def test_a_remote_resume_invents_no_pause_for_downstream_nodes(tmp_path, instrumented): + """Only the level's FIRST superstep re-runs interrupted tasks; every node + after it is ordinary downstream work at the same namespace depth. Without + that guard the resumed run manufactures one `agent_resume` + `human_input` + per node it visits, each with an id that matches no pause — and the real + pause still never closes.""" + saver = InMemorySaver() + config = {"configurable": {"thread_id": "xproc-downstream"}} + + def build(): + return build_simple( + [ + ("ask", lambda state: {"answer": str(interrupt({"prompt": "ok?"}))}), + ("after_one", lambda state: {"vals": ["a"]}), + ("after_two", lambda state: {"vals": ["b"]}), + ], + name="downstream", + checkpointer=saver, + ) + + build().invoke(empty_state(), config=config) + opened = only(read_events(tmp_path), "human_wait") + assert len(opened) == 1 + + _forget_everything_this_process_knows() + build().invoke(Command(resume="yes"), config=config) + + rows = read_events(tmp_path) + assert [r["hook_name"] for r in only(rows, "hook_triggered")][1:] == [ + "ask", + "after_one", + "after_two", + ] + assert len(only(rows, "agent_resume")) == 1 + assert len(only(rows, "human_input")) == 1 + assert only(rows, "agent_resume")[0]["pause_id"] == opened[0]["input_id"] + assert len(only(rows, "human_wait")) == 1 + assert not only(rows, "error") + + +def test_a_fresh_turn_in_a_fresh_process_is_not_a_remote_resume(tmp_path, instrumented): + """The counterweight. `_RemoteResume` is armed off the root input alone, so + a graph that simply runs — no interrupt anywhere, no `Command` — must never + manufacture a resume for a human who was never asked.""" + build_graph().invoke(empty_state(), config={"configurable": {"thread_id": "plain"}}) + rows = read_events(tmp_path) + assert not only(rows, "agent_resume") + assert not only(rows, "human_input") + + +def test_a_remote_rerun_with_no_answer_records_no_approval(tmp_path, instrumented): + """The sharp counterweight for the arming condition. + + `invoke(None, config)` re-runs an interrupted thread WITHOUT answering it — + the task interrupts again and no human said anything. Widening the arming + test from "a `Command` carrying a resume value" to "anything that continues + a thread" would record an approval, with an empty response, for a human who + is still waiting. The pause must simply stay open.""" + saver = InMemorySaver() + config = {"configurable": {"thread_id": "xproc-noanswer"}} + build_graph(checkpointer=saver).invoke(empty_state(), config=config) + + _forget_everything_this_process_knows() + build_graph(checkpointer=saver).invoke(None, config=config) + + rows = read_events(tmp_path) + assert not only(rows, "agent_resume"), "an approval was recorded for a re-run" + assert not only(rows, "human_input") + # Still waiting, and said so twice — once per attempt. + assert len(only(rows, "human_wait")) == 2 + + +def test_an_in_process_resume_emits_exactly_one_resume_pair(tmp_path, instrumented): + """The other counterweight: the remote path must stay inert whenever this + process owns the pause, or every ordinary approval is recorded twice.""" + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "in-process"}} + app.invoke(empty_state(), config=config) + app.invoke(Command(resume="yes"), config=config) + + rows = read_events(tmp_path) + assert len(only(rows, "agent_resume")) == 1 + assert len(only(rows, "human_input")) == 1 + assert only(rows, "agent_resume")[0].get("fw_resumed_elsewhere") is None + + +def test_the_interrupt_id_is_still_derived_from_the_checkpoint_namespace(): + """The whole cross-process fix rests on one langgraph invariant: an + interrupt's id is `xxh3_128` of the interrupted task's checkpoint namespace, + not a random value. If upstream makes it random, `_close_remote_pause` goes + on emitting ids that correlate with nothing and every behavioural test above + keeps passing, because they all compare our derived id against itself.""" + from langgraph.types import Interrupt + + ns = "ask:0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0" + assert Interrupt.from_ns("value", ns).id == adapter._interrupt_id_of(ns) + assert Interrupt.from_ns("a different value", ns).id == adapter._interrupt_id_of(ns) + assert adapter._interrupt_id_of(ns) != adapter._interrupt_id_of(ns + "x") + + +def test_a_subgraph_compiled_under_its_node_name_is_one_hook_not_two( + tmp_path, instrumented +): + """`sub.compile(name="child")` added as `add_node("child", sub)` is the + natural way to name a subgraph, and it makes the subgraph's own Pregel run + match the node too. That produced a duplicate `hook_triggered` for the node + AND turned the Pregel run into a node, which then also opened and closed a + nested agent on the same run — one visit rendering as four spans.""" + inner = StateGraph(State) + inner.add_node("deep", lambda state: {"vals": ["deep"]}) + inner.add_edge(START, "deep") + inner.add_edge("deep", END) + + outer = StateGraph(State) + outer.add_node("child", inner.compile(name="child")) + outer.add_edge(START, "child") + outer.add_edge("child", END) + outer.compile(name="root_graph").invoke( + empty_state(), config={"configurable": {"thread_id": "sub-name"}} + ) + + rows = read_events(tmp_path) + assert [r["hook_name"] for r in only(rows, "hook_triggered")] == ["child", "deep"] + assert [r["hook_name"] for r in only(rows, "hook_completed")] == ["deep", "child"] + assert [r["agent_id"] for r in only(rows, "agent_start")] == [ + "root_graph", + "root_graph/child", + ] + + +def test_a_leaf_run_is_never_the_nodes_own_run(tmp_path): + """The second exclusion, pinned directly. + + langgraph 1.2.11 happens to tag the inner run `seq:step:N` as well, so the + behavioural tests above would still pass with this condition removed — and + that is exactly why it is here. It states the invariant that does not depend + on a tag convention: whatever you hand `add_node`, the node's OWN run is the + `chain` run langgraph builds around it, and a `tool` / `llm` / `chat_model` / + `retriever` run carrying the node's name is the thing you passed, running + underneath. Lose this and a tag rename silently deletes tool and model + events again, which is a wrong answer with no symptom. + """ + import types as _types + + meta = {"langgraph_node": "adder"} + + def run(run_type, tags=()): + return _types.SimpleNamespace(name="adder", run_type=run_type, tags=list(tags)) + + assert adapter._node_of(run("chain"), meta) == "adder" + for leaf in ("tool", "llm", "chat_model", "retriever"): + assert adapter._node_of(run(leaf), meta) is None, ( + f"a {leaf} run named after its node was claimed as the node itself" + ) + + +def test_an_overlapping_run_cannot_strand_the_pause_it_did_not_answer( + tmp_path, instrumented +): + """The two fixes above meeting in the shape that produced both. + + `_State.sessions` is keyed by session id, so a second root under the same id + overwrites the paused run's entry and then pops it on the way out. The real + approval, arriving afterwards, finds nothing — which is the same position a + fresh worker is in, and is why the remote path is the backstop rather than a + special case. The pause must still close, on the id the human was asked + under, carrying what they actually said.""" + session = {"metadata": {adapter.SESSION_METADATA_KEY: "shared-conversation"}} + app = build_graph(checkpointer=InMemorySaver()) + config = {"configurable": {"thread_id": "stranded"}, **session} + + app.invoke(empty_state(), config=config) + opened = only(read_events(tmp_path), "human_wait") + assert len(opened) == 1 + + build_simple([("other", lambda state: {"vals": ["o"]})], name="other").invoke( + empty_state(), config=dict(session) + ) + app.invoke(Command(resume="approved"), config=config) + + rows = read_events(tmp_path) + assert {r["session_id"] for r in rows} == {"shared-conversation"} + assert len(only(rows, "agent_resume")) == 1 + answers = only(rows, "human_input") + assert len(answers) == 1 + assert answers[0]["input_id"] == opened[0]["input_id"] + assert answers[0]["response"] == "approved" diff --git a/sdk/python/tests/integrations/test_llama_index.py b/sdk/python/tests/integrations/test_llama_index.py new file mode 100644 index 000000000..1a0ce3285 --- /dev/null +++ b/sdk/python/tests/integrations/test_llama_index.py @@ -0,0 +1,1492 @@ +"""The LlamaIndex adapter, against the real framework and a stub model. + +No network, no API key, no mock of our own code: every assertion below reads +the **JSONL the writer actually produced** after driving a real +`FunctionAgent` / `Workflow`. Asserting on mock call args would happily pass +against an adapter that emits nothing the ingest pipeline can use. + +The single highest-value test in the file is +`test_our_overrides_still_exist_on_the_framework_base_classes`. Every other +test here can stay green while the adapter is completely dead: if upstream +renames `prepare_to_exit_span`, our override is never called, the span handler +records nothing, and a fake-based suite notices nothing at all. +""" + +import asyncio +import inspect +import json +import os +from dataclasses import fields as dataclass_fields +from typing import Any, List, Sequence + +import pytest + +import failproofai_sdk +from failproofai_sdk import _runtime, _schema +from failproofai_sdk.integrations import _compat, _core + +# `pytest.importorskip` is fail-open: misspell the module and every leg skips +# while CI stays green having tested nothing. The framework CI leg sets +# AGENTEYE_TESTS_REQUIRE_FRAMEWORKS=1, which turns the skip into a hard error. +if os.environ.get("AGENTEYE_TESTS_REQUIRE_FRAMEWORKS", "").strip().lower() in {"1", "true", "yes"}: + import llama_index.core # noqa: F401 +else: + pytest.importorskip("llama_index.core", reason="llama-index-core is not installed") + +from llama_index.core.base.llms.types import ( # noqa: E402 + ChatMessage, + ChatResponse, + ChatResponseAsyncGen, + ChatResponseGen, + CompletionResponse, + CompletionResponseAsyncGen, + CompletionResponseGen, + LLMMetadata, + MessageRole, +) +from llama_index.core.llms.callbacks import llm_chat_callback, llm_completion_callback # noqa: E402 +from llama_index.core.llms.function_calling import FunctionCallingLLM # noqa: E402 +from llama_index.core.tools import ToolSelection # noqa: E402 +from llama_index.core.tools.types import BaseTool # noqa: E402 +from pydantic import Field, PrivateAttr # noqa: E402 + +from failproofai_sdk.integrations import llama_index as adapter_module # noqa: E402 + +pytestmark = pytest.mark.framework + + +# --------------------------------------------------------------------------- +# A stub function-calling model. Real FunctionAgent, fake LLM. +# --------------------------------------------------------------------------- + +class StubLLM(FunctionCallingLLM): + """Replays a script of tool calls, then answers. Never touches a network.""" + + model: str = "stub-model-1" + script: List[Any] = Field(default_factory=list) + final: str = "done" + _turn: int = PrivateAttr(default=0) + + @property + def metadata(self) -> LLMMetadata: + return LLMMetadata( + model_name=self.model, is_chat_model=True, is_function_calling_model=True + ) + + def _next(self) -> ChatResponse: + turn = self._turn + self._turn += 1 + if turn < len(self.script): + name, kwargs = self.script[turn] + message = ChatMessage( + role=MessageRole.ASSISTANT, + content="", + additional_kwargs={ + "tool_calls": [{"id": f"call_{turn}", "name": name, "kwargs": kwargs}] + }, + ) + else: + message = ChatMessage(role=MessageRole.ASSISTANT, content=self.final) + return ChatResponse( + message=message, + raw={"usage": {"prompt_tokens": 11, "completion_tokens": 7, "total_tokens": 18}}, + ) + + @llm_chat_callback() + def chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatResponse: + return self._next() + + @llm_chat_callback() + async def achat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatResponse: + return self._next() + + @llm_chat_callback() + def stream_chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatResponseGen: + def gen(): + yield self._next() + + return gen() + + @llm_chat_callback() + async def astream_chat( + self, messages: Sequence[ChatMessage], **kwargs: Any + ) -> ChatResponseAsyncGen: + async def gen(): + yield self._next() + + return gen() + + @llm_completion_callback() + def complete(self, prompt: str, formatted: bool = False, **kwargs: Any) -> CompletionResponse: + return CompletionResponse(text=self.final) + + @llm_completion_callback() + async def acomplete( + self, prompt: str, formatted: bool = False, **kwargs: Any + ) -> CompletionResponse: + return CompletionResponse(text=self.final) + + @llm_completion_callback() + def stream_complete( + self, prompt: str, formatted: bool = False, **kwargs: Any + ) -> CompletionResponseGen: + def gen(): + yield CompletionResponse(text=self.final, delta=self.final) + + return gen() + + @llm_completion_callback() + async def astream_complete( + self, prompt: str, formatted: bool = False, **kwargs: Any + ) -> CompletionResponseAsyncGen: + async def gen(): + yield CompletionResponse(text=self.final, delta=self.final) + + return gen() + + def _prepare_chat_with_tools( + self, + tools: Sequence["BaseTool"], + user_msg: Any = None, + chat_history: Any = None, + verbose: bool = False, + allow_parallel_tool_calls: bool = False, + tool_required: bool = False, + **kwargs: Any, + ) -> dict: + messages = list(chat_history or []) + if user_msg is not None: + messages.append( + ChatMessage(role=MessageRole.USER, content=user_msg) + if isinstance(user_msg, str) + else user_msg + ) + return {"messages": messages, **kwargs} + + def get_tool_calls_from_response( + self, response: ChatResponse, error_on_no_tool_call: bool = True, **kwargs: Any + ) -> List[ToolSelection]: + calls = response.message.additional_kwargs.get("tool_calls", []) + if not calls and error_on_no_tool_call: + raise ValueError("no tool calls") + return [ + ToolSelection(tool_id=c["id"], tool_name=c["name"], tool_kwargs=c["kwargs"]) + for c in calls + ] + + +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + +def boom(x: int) -> int: + """Always raises.""" + raise RuntimeError("tool exploded") + + +async def _await(awaitable): + return await awaitable + + +def drive(coro_factory): + """Run a workflow from sync test code. + + `Workflow.run()` schedules tasks eagerly, so it must be *called* with a + loop already running — `asyncio.run(workflow.run())` raises + "no running event loop" before the adapter ever sees anything. + """ + + async def _main(): + return await coro_factory() + + return asyncio.run(_main()) + + +def run_agent(agent, prompt: str) -> str: + return str(drive(lambda: agent.run(prompt))) + + +def run_workflow(workflow, **kwargs) -> str: + return str(drive(lambda: workflow.run(**kwargs))) + + +def calculator(llm, **kwargs): + from llama_index.core.agent.workflow import FunctionAgent + + kwargs.setdefault("tools", [add]) + kwargs.setdefault("streaming", False) + return FunctionAgent(name="calc", description="does math", llm=llm, **kwargs) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture() +def instrumented(tmp_path): + """Instrument for one test, then put everything back. + + The flush interval is enormous on purpose: the writer names files with + millisecond resolution, so two automatic flushes inside the same + millisecond overwrite each other. Tests flush once, explicitly, at the end. + """ + _core.set_strict(False) + _compat.set_strict_integrations(False) + _core.reset_failures() + _runtime.writer.set_flush_interval(3600) + assert failproofai_sdk.instrument("llama_index") == ("llama_index",) + try: + yield adapter_module.adapter + finally: + failproofai_sdk.uninstrument("llama_index") + _core.set_strict(None) + _compat.set_strict_integrations(None) + _core.reset_failures() + + +@pytest.fixture() +def instrumented_without_steps(tmp_path): + """`instrumented`, with the workflow-step hooks switched off. + + Spelled out rather than parameterising the fixture above: `instrumented` is + used by nearly every test in this file, and a signature change there is a + change to all of them. + """ + _core.set_strict(False) + _compat.set_strict_integrations(False) + _core.reset_failures() + _runtime.writer.set_flush_interval(3600) + assert failproofai_sdk.instrument("llama_index", steps=False) == ("llama_index",) + try: + yield adapter_module.adapter + finally: + failproofai_sdk.uninstrument("llama_index") + _core.set_strict(None) + _compat.set_strict_integrations(None) + _core.reset_failures() + + +@pytest.fixture(autouse=True) +def _isolate_writer_queue(): + """Start and end every test with an empty writer queue. + + The queue is process-global while `base_dir` is per-test, so events a test + never flushed are written into the NEXT test's directory the first time + anything flushes — which reads as a mystery second session id and is + exactly the kind of cross-test bleed this suite exists to rule out. + """ + _runtime.writer._queue.clear() + yield + _runtime.writer._queue.clear() + + +@pytest.fixture(autouse=True) +def _no_silent_adapter_failure(request): + """Fail the test if the adapter swallowed an exception. + + `safe()` exists so a bug costs one log line instead of the process — which + also means a broken adapter passes every behavioural test that only checks + "the run finished". This makes that invisible failure visible in the suite. + """ + yield + if _EXPECTS_FAILURE in request.node.name: + return + assert not _core._disabled, f"adapter self-disabled a call site: {_core._disabled}" + assert not _core._failures, f"adapter swallowed exceptions: {_core._failures}" + + +# pyproject sets `--strict-markers`, so a bespoke marker is not available here: +# the two tests that deliberately make the adapter fail are named instead. +_EXPECTS_FAILURE = "raises_on_every_call" + + +def read_events(tmp_path) -> list[dict]: + _runtime.writer.flush_now() + rows: list[dict] = [] + for path in sorted((tmp_path / "events").glob("*.jsonl")): + rows.extend(json.loads(line) for line in path.read_text().splitlines() if line) + return rows + + +def types_of(events: list[dict]) -> list[str]: + return [event["type"] for event in events] + + +def only(events: list[dict], kind: str) -> list[dict]: + return [event for event in events if event["type"] == kind] + + +# The exact sequence a one-tool FunctionAgent turn produces. Written out rather +# than computed: a change here should require a human to look at it. +ONE_TOOL_SEQUENCE = [ + "agent_start", + "hook_triggered", # init_run + "hook_completed", + "hook_triggered", # setup_agent + "hook_completed", + "hook_triggered", # run_agent_step + "model_request", + "model_response", + "hook_completed", + "hook_triggered", # parse_agent_output + "hook_completed", + "hook_triggered", # call_tool + "tool_use", + "tool_result", + "hook_completed", + "hook_triggered", # aggregate_tool_results + "hook_completed", + "hook_triggered", # setup_agent + "hook_completed", + "hook_triggered", # run_agent_step + "model_request", + "model_response", + "hook_completed", + "hook_triggered", # parse_agent_output + "hook_completed", + "agent_end", +] + + +# --------------------------------------------------------------------------- +# The representative run +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("streaming", [False, True], ids=["blocking", "streaming"]) +def test_exact_event_sequence_for_a_one_tool_run(instrumented, tmp_path, streaming): + llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5") + agent = calculator(llm, streaming=streaming) + assert str(run_agent(agent, "what is 2+3?")) == "5" + + events = read_events(tmp_path) + # Streaming produces the same shape: the model span exits the instant the + # generator is created, so the response is parked and closed by + # LLMChatEndEvent rather than by the span. + assert types_of(events) == ONE_TOOL_SEQUENCE + + +def test_the_root_agent_start_is_the_sessions_first_event(instrumented, tmp_path): + llm = StubLLM(script=[("add", {"a": 1, "b": 1})], final="2") + run_agent(calculator(llm), "1+1?") + + events = read_events(tmp_path) + # `agent_sessions.agent_id = any(...)` over an ORDER BY (session_id, ts) + # table returns the FIRST agent_id by time. If anything preceded the root + # agent_start the sessions list would name a workflow step instead. + assert events[0]["type"] == "agent_start" + assert events[0]["agent_id"] == "calc" + assert {event["session_id"] for event in events} == {events[0]["session_id"]} + + +def test_every_event_carries_the_framework_triple(instrumented, tmp_path): + llm = StubLLM(script=[("add", {"a": 1, "b": 1})], final="2") + run_agent(calculator(llm), "1+1?") + + for event in read_events(tmp_path): + assert event["framework"] == "llama_index" + assert event["framework_version"] + assert event["integration_version"] + + +def test_agent_ids_are_readable_names_not_span_ids(instrumented, tmp_path): + llm = StubLLM(script=[("add", {"a": 1, "b": 1})], final="2") + run_agent(calculator(llm), "1+1?") + + for event in read_events(tmp_path): + agent_id = event["agent_id"] + # agent_id is LowCardinality(String) and the global dashboard facet; a + # span id in it fills the filter dropdown with one entry per run. + assert agent_id == "calc" + assert "-" not in agent_id and len(agent_id) < 32 + + +def test_model_requests_and_responses_pair_on_request_id(instrumented, tmp_path): + llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5") + run_agent(calculator(llm), "2+3?") + + events = read_events(tmp_path) + requests = only(events, "model_request") + responses = only(events, "model_response") + assert len(requests) == len(responses) == 2 + assert [r["request_id"] for r in requests] == [r["request_id"] for r in responses] + assert len({r["request_id"] for r in requests}) == 2 + + for response in responses: + # Invariant 3: an int, always. The server stores duration_ms as a u32 + # and its JSON parser drops floats, so a float silently NULLs it. + assert isinstance(response["duration_ms"], int) + assert not isinstance(response["duration_ms"], bool) + assert response["model"] == "stub-model-1" + + +def test_model_name_comes_from_metadata_not_the_gutted_model_dict(instrumented, tmp_path): + llm = StubLLM(script=[], final="hi") + run_agent(calculator(llm), "hi") + + events = read_events(tmp_path) + assert {e["model"] for e in only(events, "model_request")} == {"stub-model-1"} + # `to_payload()` replaced `to_dict()` in 0.14.23 and the "model" key is gone. + start_payload = llm.to_payload() + assert "model" not in start_payload + + +def test_tokens_are_extracted_and_the_raw_usage_dict_ships_too(instrumented, tmp_path): + llm = StubLLM(script=[], final="hi") + run_agent(calculator(llm), "hi") + + response = only(read_events(tmp_path), "model_response")[0] + assert response["input_tokens"] == 11 + assert response["output_tokens"] == 7 + # Both event_summary.rs and sessionSummary.ts fall back to `usage`. + assert response["usage"]["total_tokens"] == 18 + + +def test_tool_use_and_tool_result_pair_and_carry_a_duration(instrumented, tmp_path): + llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5") + run_agent(calculator(llm), "2+3?") + + events = read_events(tmp_path) + uses = only(events, "tool_use") + results = only(events, "tool_result") + assert len(uses) == len(results) == 1 + assert uses[0]["tool_call_id"] == results[0]["tool_call_id"] == "call_0" + assert uses[0]["tool_name"] == results[0]["tool_name"] == "add" + assert uses[0]["input"] == {"a": 2, "b": 3} + assert results[0]["output"] == "5" + assert isinstance(results[0]["duration_ms"], int) + + +def test_tool_call_id_is_the_frameworks_own_id(instrumented, tmp_path): + llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5") + run_agent(calculator(llm), "2+3?") + + use = only(read_events(tmp_path), "tool_use")[0] + # Passing the framework's id through verbatim is what makes our events line + # up with the customer's provider logs. + assert use["tool_call_id"] == "call_0" + assert use["fw_tool_id"] == "call_0" + + +def test_workflow_steps_become_hooks_not_agents(instrumented, tmp_path): + llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5") + run_agent(calculator(llm), "2+3?") + + events = read_events(tmp_path) + hooks = only(events, "hook_triggered") + names = [hook["hook_name"] for hook in hooks] + assert names[:3] == ["init_run", "setup_agent", "run_agent_step"] + assert all(hook["trigger_event"] == "workflow_step" for hook in hooks) + # Steps as agents would drown the agent_id facet with `parse_agent_output`. + assert {e["agent_id"] for e in events} == {"calc"} + for completed in only(events, "hook_completed"): + assert isinstance(completed["duration_ms"], int) + + +# --------------------------------------------------------------------------- +# Rendering invariants +# --------------------------------------------------------------------------- + +def assert_rendering_invariants(events: list[dict]) -> None: + """The four properties that make the dashboard draw the session correctly.""" + assert events, "no events at all" + assert events[0]["type"] == "agent_start", "invariant 2: root agent_start must be first" + + open_agents: dict[str, int] = {} + open_leaves: dict[tuple[str, str], int] = {} + for event in events: + kind = event["type"] + agent_id = event["agent_id"] + if kind == "agent_start": + open_agents[agent_id] = open_agents.get(agent_id, 0) + 1 + else: + # Invariant 1: an event under an agent_id with no open agent_start + # makes executionGraph synthesize a root span that stays `ongoing`. + assert open_agents.get(agent_id, 0) > 0, f"{kind} under a closed agent {agent_id!r}" + if kind == "agent_end": + open_agents[agent_id] -= 1 + elif kind == "tool_use": + open_leaves[("tool", event["tool_call_id"])] = 1 + elif kind == "tool_result": + tool_leaf = open_leaves.pop(("tool", event["tool_call_id"]), None) + assert tool_leaf, "unpaired tool_result" + elif kind == "model_request": + open_leaves[("model", event["request_id"])] = 1 + elif kind == "model_response": + model_leaf = open_leaves.pop(("model", event["request_id"]), None) + assert model_leaf, "unpaired model_response" + assert isinstance(event["duration_ms"], int), "invariant 3" + elif kind == "hook_triggered": + open_leaves[("hook", event["hook_id"])] = 1 + elif kind == "hook_completed": + hook_leaf = open_leaves.pop(("hook", event["hook_id"]), None) + assert hook_leaf, "unpaired hook_completed" + + assert not any(open_agents.values()), f"agents left open: {open_agents}" + # Invariant 4: agent_end force-closes open pauses but NOT tools or models, + # so a leaf left open renders the session `ongoing` forever. + assert not open_leaves, f"leaves left open: {sorted(open_leaves)}" + + +@pytest.mark.parametrize("streaming", [False, True], ids=["blocking", "streaming"]) +def test_rendering_invariants_hold_for_a_normal_run(instrumented, tmp_path, streaming): + llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5") + run_agent(calculator(llm, streaming=streaming), "2+3?") + assert_rendering_invariants(read_events(tmp_path)) + + +def test_rendering_invariants_hold_when_a_tool_fails(instrumented, tmp_path): + llm = StubLLM(script=[("boom", {"x": 1})], final="recovered") + agent = calculator(llm, tools=[boom]) + run_agent(agent, "blow up") + assert_rendering_invariants(read_events(tmp_path)) + + +# --------------------------------------------------------------------------- +# Failure paths +# --------------------------------------------------------------------------- + +def test_a_failing_tool_is_reported_on_the_tool_and_not_double_counted(instrumented, tmp_path): + llm = StubLLM(script=[("boom", {"x": 1})], final="recovered") + result = run_agent(calculator(llm, tools=[boom]), "blow up") + assert str(result) == "recovered" + + events = read_events(tmp_path) + result_event = only(events, "tool_result")[0] + assert result_event["error"] == "RuntimeError: tool exploded" + # The span that owns the failure reports it. A standalone `error` event + # would double-count on sessionSummary.errorCount. + assert only(events, "error") == [] + # The agent recovered, so the run did not fail. + assert only(events, "agent_end")[0]["outcome"] == "success" + + +def test_a_failing_workflow_step_reports_once_and_fails_the_agent(instrumented, tmp_path): + from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step + + class Bad(Workflow): + @step + async def go(self, ev: StartEvent) -> StopEvent: + raise ValueError("workflow blew up") + + with pytest.raises(ValueError): + run_workflow(Bad(timeout=5)) + + events = read_events(tmp_path) + completed = only(events, "hook_completed")[0] + assert completed["outcome"] == "failed" + assert completed["error"] == "ValueError: workflow blew up" + assert only(events, "error") == [], "the step owns this failure; do not count it twice" + assert only(events, "agent_end")[0]["outcome"] == "failed" + assert_rendering_invariants(events) + + +def test_a_run_level_failure_nobody_owns_gets_one_error_event(instrumented, tmp_path): + from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step + + class Slow(Workflow): + @step + async def go(self, ev: StartEvent) -> StopEvent: + await asyncio.sleep(5) + return StopEvent(result="never") + + with pytest.raises(Exception): + run_workflow(Slow(timeout=0.3)) + + events = read_events(tmp_path) + errors = only(events, "error") + # Nothing below the run reported the timeout, so exactly one standalone + # `error` carries it — and it comes strictly BEFORE agent_end, because the + # graph closes the agent span at agent_end and an error after it is + # attributed to nothing. + assert len(errors) == 1 + assert errors[0]["error_type"] == "WorkflowTimeoutError" + assert types_of(events).index("error") < types_of(events).index("agent_end") + assert only(events, "agent_end")[0]["outcome"] == "failed" + assert_rendering_invariants(events) + + +def test_a_failed_agent_end_says_what_killed_the_run(instrumented, tmp_path): + """`outcome="failed"` is not a reason, and `summary` is a promoted column. + + The failing step's `hook_completed` does carry the error, but that is + payload-only — and with `steps=False` it is not emitted at all, which leaves + a failed run with its cause recorded precisely nowhere. + """ + from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step + + class Bad(Workflow): + @step + async def go(self, ev: StartEvent) -> StopEvent: + raise ValueError("workflow blew up") + + with pytest.raises(ValueError): + run_workflow(Bad(timeout=5)) + + end = only(read_events(tmp_path), "agent_end")[0] + assert end["outcome"] == "failed" + assert end["summary"] == "ValueError: workflow blew up" + + +def test_a_timed_out_agent_end_says_what_killed_the_run(instrumented, tmp_path): + """Same promise on the other failure path, where there IS an error event.""" + from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step + + class Slow(Workflow): + @step + async def go(self, ev: StartEvent) -> StopEvent: + await asyncio.sleep(5) + return StopEvent(result="never") + + with pytest.raises(Exception): + run_workflow(Slow(timeout=0.3)) + + end = only(read_events(tmp_path), "agent_end")[0] + assert end["outcome"] == "failed" + assert end["summary"].startswith("WorkflowTimeoutError:") + + +def test_a_translator_that_raises_on_every_call_does_not_break_the_run( + instrumented, tmp_path, monkeypatch, caplog +): + """The whole failure policy, proved rather than asserted. + + This also covers the LlamaIndex-specific hazard: the dispatcher wraps every + handler call in `except BaseException: pass` **with no logging**, so an + adapter bug is invisible unless we log it ourselves. + """ + state = instrumented.state + + def explode(*args, **kwargs): + raise RuntimeError("translator is broken") + + monkeypatch.setattr(type(state), "span_enter", explode) + monkeypatch.setattr(type(state), "span_exit", explode) + monkeypatch.setattr(type(state), "span_drop", explode) + monkeypatch.setattr(type(state), "model_start", explode) + monkeypatch.setattr(type(state), "model_end", explode) + + caplog.set_level("WARNING", logger="failproofai_sdk.integrations") + llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5") + assert str(run_agent(calculator(llm), "2+3?")) == "5" + + logged = " ".join(record.getMessage() for record in caplog.records) + # BOTH handlers, named individually: the span handler alone satisfying this + # would let an unwrapped `handle()` through, and the event handler is where + # every model event comes from. + # Match the qualname exactly: `.handle` alone is a substring of + # `.handler_classes`, which appears in every site name in this module. + assert "FailproofAISpanHandler.new_span" in logged, ( + "the span handler swallowed its exception silently" + ) + assert "FailproofAIEventHandler.handle" in logged, ( + "the EVENT handler swallowed its exception without logging — the " + "dispatcher already does that for us, with no traceback and no name" + ) + _core.reset_failures() + + +def test_strict_mode_still_cannot_take_the_run_down(instrumented, tmp_path, monkeypatch): + """FAILPROOFAI_SDK_STRICT re-raises out of `safe()` — and the dispatcher eats it. + + Worth pinning: strict mode is a debugging switch, not a way to make an + instrumented LlamaIndex application fail fast. Anyone reaching for it to + "make errors loud in prod" should read this test first. + """ + state = instrumented.state + _core.set_strict(True) + + def explode(*args, **kwargs): + raise RuntimeError("translator is broken") + + monkeypatch.setattr(type(state), "span_enter", explode) + llm = StubLLM(script=[], final="ok") + assert str(run_agent(calculator(llm), "hi")) == "ok" + + +# --------------------------------------------------------------------------- +# Human in the loop +# --------------------------------------------------------------------------- + +def test_human_in_the_loop_emits_both_pairs(instrumented, tmp_path): + from llama_index.core.workflow import Context, HumanResponseEvent, InputRequiredEvent + + async def ask_human(ctx: Context, question: str) -> str: + """Ask the human a question.""" + answer = await ctx.wait_for_event( + HumanResponseEvent, + waiter_id="ask", + waiter_event=InputRequiredEvent(prefix=question), + ) + return answer.response + + async def converse() -> str: + llm = StubLLM(script=[("ask_human", {"question": "ok?"})], final="the human said yes") + agent = calculator(llm, tools=[ask_human]) + handler = agent.run("ask them") + async for event in handler.stream_events(): + if isinstance(event, InputRequiredEvent): + handler.ctx.send_event(HumanResponseEvent(response="yes")) + return str(await handler) + + assert asyncio.run(converse()) == "the human said yes" + + events = read_events(tmp_path) + kinds = types_of(events) + # Both pairs, in this order. Only agent_pause/agent_resume feeds the + # graph's paused time; only human_wait/human_input carries the prompt and + # the pending-human badge. Neither alone is sufficient. + assert kinds.index("human_wait") < kinds.index("agent_pause") + assert kinds.index("agent_pause") < kinds.index("agent_resume") + assert kinds.index("agent_resume") < kinds.index("human_input") + + wait = only(events, "human_wait")[0] + pause = only(events, "agent_pause")[0] + resume = only(events, "agent_resume")[0] + given = only(events, "human_input")[0] + assert wait["input_id"] == pause["pause_id"] == resume["pause_id"] == given["input_id"] + assert wait["prompt"] and "ok?" in wait["prompt"] + assert isinstance(resume["duration_ms"], int) + assert isinstance(given["duration_ms"], int) + assert_rendering_invariants(events) + + +def test_a_paused_tool_does_not_collide_with_its_retry(instrumented, tmp_path): + """LlamaIndex re-runs a paused tool from scratch, reusing its tool_id. + + Two `tool_use`/`tool_result` pairs sharing one `tool_call_id` in a session + would pair wrongly and report a nonsense duration, so the retry is suffixed. + """ + from llama_index.core.workflow import Context, HumanResponseEvent, InputRequiredEvent + + async def ask_human(ctx: Context, question: str) -> str: + """Ask the human a question.""" + answer = await ctx.wait_for_event( + HumanResponseEvent, + waiter_id="ask", + waiter_event=InputRequiredEvent(prefix=question), + ) + return answer.response + + async def converse() -> None: + llm = StubLLM(script=[("ask_human", {"question": "ok?"})], final="done") + handler = calculator(llm, tools=[ask_human]).run("ask them") + async for event in handler.stream_events(): + if isinstance(event, InputRequiredEvent): + handler.ctx.send_event(HumanResponseEvent(response="yes")) + await handler + + asyncio.run(converse()) + + events = read_events(tmp_path) + ids = [event["tool_call_id"] for event in only(events, "tool_use")] + assert ids == ["call_0", "call_0#1"] + assert [e["tool_call_id"] for e in only(events, "tool_result")] == ids + assert only(events, "tool_result")[0]["fw_closed_by"] == "human_wait" + + +# --------------------------------------------------------------------------- +# Structure +# --------------------------------------------------------------------------- + +def test_a_nested_workflow_becomes_a_nested_agent(instrumented, tmp_path): + from llama_index.core.workflow import Event, StartEvent, StopEvent, Workflow, step + + class Mid(Event): + payload: str + + class Inner(Workflow): + @step + async def go(self, ev: StartEvent) -> StopEvent: + return StopEvent(result="inner-done") + + class Outer(Workflow): + @step + async def first(self, ev: StartEvent) -> Mid: + return Mid(payload="hi") + + @step + async def second(self, ev: Mid) -> StopEvent: + return StopEvent(result=str(await Inner(timeout=5).run())) + + assert str(run_workflow(Outer(timeout=5))) == "inner-done" + + events = read_events(tmp_path) + starts = only(events, "agent_start") + assert [start["agent_id"] for start in starts] == ["Outer", "Inner"] + assert "parent_id" not in starts[0] + assert starts[1]["parent_id"] == "Outer" + assert len({event["session_id"] for event in events}) == 1 + assert_rendering_invariants(events) + + +# --------------------------------------------------------------------------- +# AgentWorkflow handoffs +# +# `AgentWorkflow` does NOT run its agents as nested workflows: there is one +# `AgentWorkflow.run` span and the agents are steps inside it. Read off the span +# tree alone a two-agent crew is one flat `agent_id="AgentWorkflow"` and the +# handoff is invisible, so the adapter keys nested agents off the +# `current_agent_name` the runtime puts on every AgentInput/AgentSetup/ +# AgentOutput instead. +# --------------------------------------------------------------------------- + +HANDOFF_TO_ANALYST = ("handoff", {"to_agent": "analyst", "reason": "over to you"}) +HANDOFF_TO_RESEARCHER = ("handoff", {"to_agent": "researcher", "reason": "back to you"}) + + +def crew(researcher_llm, analyst_llm, *, handoff_back=False): + """A real two-agent `AgentWorkflow` — the API LlamaIndex documents.""" + from llama_index.core.agent.workflow import AgentWorkflow, FunctionAgent + + researcher = FunctionAgent( + name="researcher", + description="looks numbers up", + tools=[add], + llm=researcher_llm, + streaming=False, + can_handoff_to=["analyst"], + ) + analyst = FunctionAgent( + name="analyst", + description="does the maths", + tools=[add], + llm=analyst_llm, + streaming=False, + # `None` here would mean "may hand off to anyone", which loops. + can_handoff_to=["researcher"] if handoff_back else [], + ) + return AgentWorkflow(agents=[researcher, analyst], root_agent="researcher") + + +def test_an_agent_workflow_handoff_is_two_nested_agents_not_one_flat_one( + instrumented, tmp_path +): + """The names a customer facets by are `researcher` and `analyst`. + + Flattened, every event in the session carries `agent_id="AgentWorkflow"` and + the two agents are distinguishable only by a payload extra, which + `agent_id`-keyed surfaces cannot group by at all. + """ + workflow = crew( + StubLLM(script=[("add", {"a": 1, "b": 1}), HANDOFF_TO_ANALYST]), + StubLLM(script=[("add", {"a": 2, "b": 3})], final="5"), + ) + assert str(run_workflow(workflow, user_msg="add things")) == "5" + + events = read_events(tmp_path) + starts = only(events, "agent_start") + assert [start["agent_id"] for start in starts] == [ + "AgentWorkflow", + "researcher", + "analyst", + ] + assert "parent_id" not in starts[0] + assert starts[1]["parent_id"] == "AgentWorkflow" + assert starts[2]["parent_id"] == "AgentWorkflow" + assert len({event["session_id"] for event in events}) == 1 + + # Sticky, and this is the subtle half: `ToolCall` carries no + # `current_agent_name`, so a `call_tool` step has to keep whichever agent + # asked for the tool. + assert [(e["agent_id"], e["tool_name"]) for e in only(events, "tool_use")] == [ + ("researcher", "add"), + ("researcher", "handoff"), + ("analyst", "add"), + ] + assert {e["agent_id"] for e in only(events, "model_request")} == { + "researcher", + "analyst", + } + assert_rendering_invariants(events) + + +def test_a_standalone_function_agent_does_not_nest_inside_itself(instrumented, tmp_path): + """The guard on the rule above, and the reason it is `name != agent_id`. + + A standalone `FunctionAgent.run` drives those same `AgentWorkflow` steps + with its OWN name in `current_agent_name`. Without the guard every + single-agent run would open a `calc` nested inside a `calc` — doubling the + agent count on every LlamaIndex session in the product. + """ + llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5") + run_agent(calculator(llm), "2+3?") + + events = read_events(tmp_path) + assert [start["agent_id"] for start in only(events, "agent_start")] == ["calc"] + assert types_of(events) == ONE_TOOL_SEQUENCE + assert_rendering_invariants(events) + + +def test_a_handoff_back_opens_the_first_agent_again_as_a_second_turn( + instrumented, tmp_path +): + """A -> B -> A: two turns for `researcher`, each opened and closed on its own. + + The nested agent is keyed per turn rather than per name. Reusing the key of + the `researcher` we already ended would collide in the tracker, and one of + the two `agent_start`s would never be closed. + """ + workflow = crew( + StubLLM(script=[HANDOFF_TO_ANALYST], final="done"), + StubLLM(script=[HANDOFF_TO_RESEARCHER]), + handoff_back=True, + ) + assert str(run_workflow(workflow, user_msg="round trip")) == "done" + + events = read_events(tmp_path) + assert [start["agent_id"] for start in only(events, "agent_start")] == [ + "AgentWorkflow", + "researcher", + "analyst", + "researcher", + ] + # Inner-first, and every open closed: the invariant check below fails on an + # agent left open, which is what a key collision would produce. + assert [end["agent_id"] for end in only(events, "agent_end")] == [ + "researcher", + "analyst", + "researcher", + "AgentWorkflow", + ] + assert_rendering_invariants(events) + + +def test_sub_agents_are_still_attributed_with_the_step_hooks_off( + instrumented_without_steps, tmp_path +): + """`steps=False` drops the hook pairs, not the agents. + + The sub-agent is resolved when the step span OPENS, which is a different + code path from the `hook_triggered` the option suppresses — so it is worth + proving rather than assuming. + """ + workflow = crew( + StubLLM(script=[HANDOFF_TO_ANALYST]), + StubLLM(script=[("add", {"a": 2, "b": 3})], final="5"), + ) + assert str(run_workflow(workflow, user_msg="add things")) == "5" + + events = read_events(tmp_path) + assert only(events, "hook_triggered") == [] + assert only(events, "hook_completed") == [] + assert [start["agent_id"] for start in only(events, "agent_start")] == [ + "AgentWorkflow", + "researcher", + "analyst", + ] + assert [(e["agent_id"], e["tool_name"]) for e in only(events, "tool_use")] == [ + ("researcher", "handoff"), + ("analyst", "add"), + ] + assert_rendering_invariants(events) + + +def test_two_concurrent_runs_do_not_mix(instrumented, tmp_path): + """Two overlapping runs, one process, one dispatcher, no contextvars. + + The whole reason `RunTracker` passes `session_id=` explicitly instead of + reading a contextvar: a start and its end are separate dispatcher calls, + and interleaved runs would otherwise attribute events to whichever run + happened to touch the variable last. + """ + + async def both(): + left = calculator(StubLLM(script=[("add", {"a": 1, "b": 1})], final="2")) + right = calculator(StubLLM(script=[("add", {"a": 3, "b": 4})], final="7")) + return await asyncio.gather(left.run("1+1?"), right.run("3+4?")) + + answers = {str(answer) for answer in asyncio.run(both())} + assert answers == {"2", "7"} + + events = read_events(tmp_path) + sessions = {event["session_id"] for event in events} + assert len(sessions) == 2 + + for session in sessions: + rows = [event for event in events if event["session_id"] == session] + assert types_of(rows) == ONE_TOOL_SEQUENCE + assert_rendering_invariants(rows) + # Each session saw exactly one of the two tool calls, whole. + inputs = [event["input"] for event in rows if event["type"] == "tool_use"] + assert inputs in ([{"a": 1, "b": 1}], [{"a": 3, "b": 4}]) + + +def test_a_retrieval_becomes_a_tool_with_a_summarized_output(instrumented, tmp_path): + from llama_index.core.base.base_retriever import BaseRetriever + from llama_index.core.schema import NodeWithScore, QueryBundle, TextNode + from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step + + class Fake(BaseRetriever): + def _retrieve(self, query_bundle: QueryBundle): + return [ + NodeWithScore(node=TextNode(text="x" * 5000, id_=f"n{i}"), score=1.0) + for i in range(12) + ] + + class Rag(Workflow): + @step + async def go(self, ev: StartEvent) -> StopEvent: + return StopEvent(result=f"{len(Fake().retrieve('what?'))} nodes") + + assert str(run_workflow(Rag(timeout=5))) == "12 nodes" + + events = read_events(tmp_path) + use = only(events, "tool_use")[0] + result = only(events, "tool_result")[0] + assert use["tool_name"] == "Fake" + assert use["input"] == {"query": "what?"} + assert use["fw_kind"] == "retrieval" + # Retrieved documents are the largest strings in the process and the + # payload is not a promoted column: summarize, never ship them whole. + assert result["output"]["num_nodes"] == 12 + assert len(result["output"]["top"]) == 5 + assert len(result["output"]["top"][0]["text"]) <= 200 + assert_rendering_invariants(events) + + +def test_embeddings_are_off_by_default(instrumented, tmp_path): + assert instrumented.state.embeddings is False + + +def test_no_event_carries_an_extra_that_shadows_a_declared_field(instrumented, tmp_path): + """`_schema._build()` merges extras LAST, at the top level. + + So an extra called `tool_name`, `model`, `outcome` or `input_tokens` + silently overwrites the declared field — changing the promoted the events store + column and the server's computed summary while every test still passes. + """ + llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5") + run_agent(calculator(llm), "2+3?") + + by_type = {} + for name, obj in vars(_schema).items(): + if name.endswith("Event") and hasattr(obj, "__dataclass_fields__"): + kind = "".join("_" + c.lower() if c.isupper() else c for c in name[:-5]).lstrip("_") + by_type[kind] = {f.name for f in dataclass_fields(obj)} - {"extra_fields"} + + for event in read_events(tmp_path): + declared = by_type[event["type"]] | {"type", "environment"} + extras = set(event) - declared + assert not (extras & _core.FORBIDDEN_EXTRAS), f"{event['type']} shadows {extras}" + for key in extras: + assert key.startswith("fw_") or key in _core.ALLOWED_TOP_LEVEL, ( + f"{event['type']} carries un-namespaced extra {key!r}" + ) + + +# --------------------------------------------------------------------------- +# Teardown +# --------------------------------------------------------------------------- + +def test_uninstrument_detaches_only_our_handlers(tmp_path): + from llama_index.core.instrumentation import get_dispatcher + from llama_index_instrumentation.event_handlers.base import BaseEventHandler + + class Foreign(BaseEventHandler): + def handle(self, event, **kwargs): + return None + + dispatcher = get_dispatcher() + foreign = Foreign() + dispatcher.add_event_handler(foreign) + before_events = list(dispatcher.event_handlers) + before_spans = list(dispatcher.span_handlers) + try: + failproofai_sdk.instrument("llama_index") + assert len(dispatcher.event_handlers) == len(before_events) + 1 + assert len(dispatcher.span_handlers) == len(before_spans) + 1 + failproofai_sdk.uninstrument("llama_index") + # In-place slice assignment: `add_span_handler` does `+= [h]`, so a + # plain `=` rebinds the pydantic field and can drop someone else's. + assert list(dispatcher.event_handlers) == before_events + assert list(dispatcher.span_handlers) == before_spans + assert foreign in dispatcher.event_handlers + finally: + dispatcher.event_handlers[:] = [h for h in dispatcher.event_handlers if h is not foreign] + + +ROOT_SPAN = "Empty.run-11111111-1111-4111-8111-111111111111" +TOOL_SPAN = "FunctionTool.acall-22222222-2222-4222-8222-222222222222" +LLM_SPAN = "StubLLM.astream_chat-44444444-4444-4444-8444-444444444444" + + +def _bound_args(): + """A real `inspect.BoundArguments`, which is what the dispatcher passes.""" + + def target(a=None, b=None): + return None + + return inspect.signature(target).bind() + + +def _empty_workflow(): + from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step + + class Empty(Workflow): + @step + async def go(self, ev: StartEvent) -> StopEvent: + return StopEvent(result="ok") + + return Empty() + + +def _fake_tool(): + class _Metadata: + name = "adder" + + class _Tool(BaseTool): + metadata = _Metadata() + + def __call__(self, *args, **kwargs): + return None + + return _Tool() + + +def _open_a_run_holding_a_tool(state): + """Drive the real span handler to a run with one leaf still open. + + Synthetic span ids, real handler, real base class: this is the state a + process is in when a run dies mid-tool, which no cooperative workflow will + produce on demand. + """ + _, span_cls = adapter_module.handler_classes() + handler = span_cls(state=state) + bound = _bound_args() + handler.span_enter(id_=ROOT_SPAN, bound_args=bound, instance=_empty_workflow(), parent_id=None) + handler.span_enter(id_=TOOL_SPAN, bound_args=bound, instance=_fake_tool(), parent_id=ROOT_SPAN) + assert state._runs[ROOT_SPAN].open_leaves, "the tool leaf should be open" + return handler, bound + + +def test_a_run_that_ends_holding_an_open_tool_closes_it(instrumented, tmp_path): + """Invariant 4, at the run boundary. + + `agent_end` force-closes open *pauses* but not tools or models, so a run + that finishes while a leaf is open leaves the session `ongoing` forever — + and nothing else in the suite notices, because a cooperative workflow + always closes its own leaves. + """ + state = instrumented.state + handler, bound = _open_a_run_holding_a_tool(state) + + handler.span_exit(id_=ROOT_SPAN, bound_args=bound, instance=None, result=None) + + events = read_events(tmp_path) + assert types_of(events) == ["agent_start", "tool_use", "tool_result", "agent_end"] + assert only(events, "tool_result")[0]["fw_closed_by"] == "run_ended" + assert only(events, "agent_end")[0]["outcome"] == "success" + assert_rendering_invariants(events) + + +def test_shutdown_closes_every_leaf_a_dead_run_left_open(instrumented, tmp_path): + """Same invariant at the uninstrument boundary: nothing is left dangling.""" + state = instrumented.state + _open_a_run_holding_a_tool(state) + + failproofai_sdk.uninstrument("llama_index") + + events = read_events(tmp_path) + assert types_of(events) == ["agent_start", "tool_use", "tool_result", "agent_end"] + assert only(events, "tool_result")[0]["fw_closed_by"] == "uninstrument" + assert only(events, "agent_end")[0]["outcome"] == "cancelled" + assert_rendering_invariants(events) + + +def test_the_reaper_closes_a_stale_parked_stream(instrumented, tmp_path): + """A streaming response nobody consumes never gets an LLMChatEndEvent. + + Its span has already exited, so nothing else will ever close it: without + the sweep the model_request stays open and the session reads `ongoing` + forever. + """ + state = instrumented.state + event_cls, span_cls = adapter_module.handler_classes() + spans = span_cls(state=state) + events_handler = event_cls(state=state) + + from llama_index.core.instrumentation.events.llm import LLMChatStartEvent + + bound = _bound_args() + spans.span_enter(id_=ROOT_SPAN, bound_args=bound, instance=_empty_workflow(), parent_id=None) + spans.span_enter(id_=LLM_SPAN, bound_args=bound, instance=StubLLM(), parent_id=ROOT_SPAN) + events_handler.handle( + LLMChatStartEvent(span_id=LLM_SPAN, messages=[], additional_kwargs={}, model_dict={}) + ) + # The span exits the instant the generator is created; nobody consumes it. + spans.span_exit(id_=LLM_SPAN, bound_args=bound, instance=None, result=iter([])) + + assert state._leaf_run.get(LLM_SPAN) == ROOT_SPAN + state.stale_after = 0.0 + assert state.sweep() == 1 + assert state.sweep() == 0 + + response = only(read_events(tmp_path), "model_response")[0] + assert response["fw_closed_by"] == "stale" + assert isinstance(response["duration_ms"], int) + + +# --------------------------------------------------------------------------- +# Cancellation +# +# `handler.cancel_run()` does NOT drop the run span. The runtime catches its own +# `WorkflowCancelledByUser` and exits the span cleanly, with `result=None` and +# no error — "so it shows as OK rather than ERROR in traces". Read off the span +# alone, a user pressing stop is indistinguishable from a completed run, which +# is why the adapter listens for `SpanCancelledEvent`. +# --------------------------------------------------------------------------- + +def _cancel_a_run_mid_step() -> None: + """Cancel a real run while a step is in flight, the way a stop button does.""" + from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step + + started = asyncio.Event() + + class Slow(Workflow): + @step + async def go(self, ev: StartEvent) -> StopEvent: + started.set() + await asyncio.sleep(30) + return StopEvent(result="never") + + async def _main() -> None: + handler = Slow(timeout=30).run() + await asyncio.wait_for(started.wait(), timeout=5) + await handler.cancel_run() + with pytest.raises(BaseException): + await handler + + asyncio.run(_main()) + + +def test_a_cancelled_run_is_not_reported_as_a_success(instrumented, tmp_path): + """Reporting a cancellation as success inflates the completion rate. + + `cancelled` is deliberately not `failed` either: the server counts only + `error|failed|timeout|rejected` as a failure, and a stop button is neither. + """ + _cancel_a_run_mid_step() + + events = read_events(tmp_path) + assert only(events, "agent_end")[0]["outcome"] == "cancelled" + # A cancellation is not an error, so nothing may report one. + assert only(events, "error") == [] + assert_rendering_invariants(events) + + +def test_a_step_cancelled_mid_flight_is_not_reported_as_a_success( + instrumented, tmp_path +): + """Same signal one level down: the step exits with `result=None`, no error.""" + _cancel_a_run_mid_step() + + completed = only(read_events(tmp_path), "hook_completed") + assert completed, "the in-flight step still has to close" + assert [hook["outcome"] for hook in completed] == ["cancelled"] + + +# --------------------------------------------------------------------------- +# Structural anti-drift — the one that catches a silently dead adapter +# --------------------------------------------------------------------------- + +def _our_overrides(cls, base) -> set[str]: + return { + name + for name in vars(cls) + if not name.startswith("__") + and inspect.isroutine(getattr(cls, name, None)) + and hasattr(base, name) + } + + +def _named_params(func) -> set[str]: + return { + name + for name, parameter in inspect.signature(func).parameters.items() + if name not in {"self", "cls"} + and parameter.kind + not in (inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL) + } + + +def test_our_overrides_still_exist_on_the_framework_base_classes(): + """If upstream renames a callback, our override becomes DEAD CODE. + + It is never called, nothing raises, and every other test in this file still + passes because they exercise the adapter through our own objects. This is + the only test that notices. + """ + from llama_index_instrumentation.event_handlers.base import BaseEventHandler + from llama_index_instrumentation.span_handlers.base import BaseSpanHandler + + event_cls, span_cls = adapter_module.handler_classes() + + for cls, base, expected in ( + (event_cls, BaseEventHandler, {"handle"}), + (span_cls, BaseSpanHandler, {"new_span", "prepare_to_exit_span", "prepare_to_drop_span"}), + ): + overrides = _our_overrides(cls, base) + assert expected <= overrides, f"{cls.__name__} no longer overrides {expected - overrides}" + for name in overrides: + ours = getattr(cls, name) + theirs = getattr(base, name, None) + assert inspect.isroutine(theirs), f"{base.__name__}.{name} is gone" + missing = _named_params(ours) - _named_params(theirs) + assert not missing, ( + f"{cls.__name__}.{name} declares {sorted(missing)}, which " + f"{base.__name__}.{name} no longer accepts by name" + ) + + +def test_the_span_enter_to_new_span_kwarg_rename_still_holds(): + """`span_enter(parent_id=...)` calls `new_span(parent_span_id=...)`. + + Declaring the wrong one is not an error — it lands in `**kwargs`, every + parent comes through as None, and the whole trace renders flat. + """ + from llama_index_instrumentation.span_handlers.base import BaseSpanHandler + + assert "parent_id" in _named_params(BaseSpanHandler.span_enter) + assert "parent_span_id" in _named_params(BaseSpanHandler.new_span) + assert "parent_span_id" not in _named_params(BaseSpanHandler.span_enter) + + _, span_cls = adapter_module.handler_classes() + assert "parent_span_id" in _named_params(span_cls.new_span) + + +def test_every_dispatcher_event_class_we_dispatch_on_still_exists(): + """We dispatch on the event's class NAME, so these names are the API. + + A rename upstream leaves the table below looking perfectly healthy while + the adapter records nothing at all. + """ + import importlib + + modules = [ + importlib.import_module(f"llama_index.core.instrumentation.events.{name}") + for name in ("llm", "chat_engine", "retrieval", "embedding", "exception") + ] + for name in adapter_module._HANDLED_EVENTS: + assert any(hasattr(module, name) for module in modules), ( + f"{name} no longer exists in llama_index.core.instrumentation.events" + ) + + +def test_the_cancel_event_we_dispatch_on_still_exists_where_we_expect_it(): + """`CANCEL_EVENTS` has no other guard in this file, by construction. + + Every other name we dispatch on lives under + `llama_index.core.instrumentation.events.*`, which the test above walks. + This one is dispatched by the workflows RUNTIME, so that test cannot see it + — and if it is renamed or moved, nothing raises: cancelled runs quietly go + back to being reported as successes. + """ + import importlib + + from llama_index.core.instrumentation.events.base import BaseEvent + + module = importlib.import_module("workflows.runtime.types.step_function") + for name in adapter_module.CANCEL_EVENTS: + cls = getattr(module, name, None) + assert cls is not None, f"{name} is gone from {module.__name__}" + assert issubclass(cls, BaseEvent), f"{name} is no longer a dispatcher event" + # We match on the class name and pair the mark with the span_exit behind + # it using `span_id`, which the dispatcher stamps from the active span. + assert cls.class_name() == name + assert "span_id" in cls.model_fields + + # And the exception that path exists to serve, which `cancel_run()` raises. + from workflows.errors import WorkflowCancelledByUser + + assert issubclass(WorkflowCancelledByUser, BaseException) + + +@pytest.mark.parametrize( + ("module", "event", "attributes"), + [ + ("llm", "LLMChatStartEvent", ("messages", "model_dict")), + ("llm", "LLMChatEndEvent", ("messages", "response")), + ("retrieval", "RetrievalStartEvent", ("str_or_query_bundle",)), + ("retrieval", "RetrievalEndEvent", ("nodes",)), + ("exception", "ExceptionEvent", ("exception",)), + ], +) +def test_the_event_attributes_we_read_still_exist(module, event, attributes): + import importlib + + cls = getattr(importlib.import_module(f"llama_index.core.instrumentation.events.{module}"), event) + for attribute in attributes: + assert attribute in cls.model_fields, f"{event}.{attribute} is gone" + + +def test_the_waiting_for_event_signal_still_looks_like_we_think_it_does(): + """HITL hinges on a name match: `WaitingForEvent` is a pause, not an error. + + It is not exported from `workflows.errors` and has moved before, so we + match on the class name. If it is renamed, every human-in-the-loop pause + becomes a red error event and a failed run. + """ + from workflows.runtime.types.results import AddWaiter, WaitingForEvent + + assert WaitingForEvent.__name__ == "WaitingForEvent" + assert issubclass(WaitingForEvent, Exception) + assert "waiter_id" in AddWaiter.model_fields + assert "waiter_event" in AddWaiter.model_fields + assert adapter_module._is_waiting(WaitingForEvent.__new__(WaitingForEvent)) is True + assert adapter_module._is_waiting(RuntimeError("nope")) is False + + +def test_the_dispatcher_surface_we_register_on_still_exists(): + from llama_index.core.instrumentation import get_dispatcher + + dispatcher = get_dispatcher() + assert dispatcher.name == "root", "get_dispatcher() must return the ROOT dispatcher" + assert callable(dispatcher.add_event_handler) + assert callable(dispatcher.add_span_handler) + # Child dispatchers propagate upward; that is why one registration is enough. + assert get_dispatcher("llama_index.core.something").propagate is True + + +# --------------------------------------------------------------------------- +# Registry wiring +# --------------------------------------------------------------------------- + +def test_the_registry_reaches_this_adapter_by_all_its_spellings(): + from failproofai_sdk.integrations import _canonical + + for spelling in ("llama_index", "llamaindex", "llama-index", "LlamaIndex"): + assert _canonical(spelling) == "llama_index" + + +def test_instrumenting_twice_is_a_no_op(tmp_path): + from failproofai_sdk.integrations import active + + try: + assert failproofai_sdk.instrument("llama_index") == ("llama_index",) + assert failproofai_sdk.instrument("llama_index") == () + assert "llama_index" in active() + finally: + failproofai_sdk.uninstrument("llama_index") + + +def test_uninstrumenting_something_that_was_never_installed_is_a_no_op(): + assert failproofai_sdk.uninstrument("llama_index") == () + + +def test_the_adapter_joins_a_hand_written_agent_scope(instrumented, tmp_path): + """The interop story: adapter events land in the ambient session. + + `RunTracker.identity()` falls back to `failproofai_sdk.current()`, so mixing the + manual API with an adapter produces one tree rather than two. + """ + llm = StubLLM(script=[], final="5") + with failproofai_sdk.agent("planner", goal="do maths") as identity: + run_agent(calculator(llm), "2+3?") + session_id = identity.session_id + + events = read_events(tmp_path) + assert {event["session_id"] for event in events} == {session_id} + starts = only(events, "agent_start") + assert [start["agent_id"] for start in starts] == ["planner", "calc"] + assert starts[1]["parent_id"] == "planner" diff --git a/sdk/python/tests/integrations/test_pydantic_ai.py b/sdk/python/tests/integrations/test_pydantic_ai.py new file mode 100644 index 000000000..43e69b22a --- /dev/null +++ b/sdk/python/tests/integrations/test_pydantic_ai.py @@ -0,0 +1,1068 @@ +"""The Pydantic AI adapter, against the real framework and a fake model. + +Everything here runs a genuine `pydantic_ai.Agent` — `TestModel` and +`FunctionModel` ship in the package for exactly this — and then asserts on the +**JSONL the writer actually wrote**, not on mock call args. A mock-based test +of an adapter proves that the adapter calls the functions the test says it +calls, which is the one thing that was never in doubt. + +The single highest-value test in this file is `TestAntiDrift`. Every other test +here would still pass if Pydantic AI renamed `wrap_tool_execute` tomorrow: our +override would simply never be called, the framework would run fine, and we +would silently record nothing. Only reflection over the real base class catches +that. +""" + +import asyncio +import dataclasses +import inspect +import json +import os +import re +import shutil +import uuid + +import pytest + +import failproofai_sdk +from failproofai_sdk import _runtime, _schema +from failproofai_sdk.integrations import _compat, _core + +pytestmark = pytest.mark.framework + +_REQUIRE_FRAMEWORKS = os.environ.get("AGENTEYE_TESTS_REQUIRE_FRAMEWORKS", "").strip().lower() in { + "1", + "true", + "yes", + "on", +} + +try: + from pydantic_ai import Agent, RunContext + from pydantic_ai.capabilities import AbstractCapability + from pydantic_ai.messages import ModelResponse, TextPart, ToolCallPart + from pydantic_ai.models import ModelRequestContext + from pydantic_ai.models.function import FunctionModel + from pydantic_ai.models.test import TestModel + from pydantic_ai.usage import RequestUsage, RunUsage +except ImportError: # pragma: no cover - exercised only on a bare environment + # `pytest.importorskip` is fail-open: misspell the module and every test in + # the file skips while CI stays green having tested nothing. The framework + # CI leg sets AGENTEYE_TESTS_REQUIRE_FRAMEWORKS=1 to turn that into a hard + # failure. + if _REQUIRE_FRAMEWORKS: + raise + pytest.skip("pydantic-ai is not installed", allow_module_level=True) + +from failproofai_sdk.integrations import pydantic_ai as adapter # noqa: E402 +from failproofai_sdk.integrations.pydantic_ai import FailproofAI # noqa: E402 + + +# --------------------------------------------------------------------------- +# Fixtures and helpers +# --------------------------------------------------------------------------- + +@pytest.fixture() +def emitted(tmp_path): + """Read back the real JSONL the writer produced during this test. + + The flush interval goes to an hour because event filenames only have + millisecond resolution: two flushes inside the same millisecond write to the + same path and the second clobbers the first. So the background thread is + parked and every flush in this file is explicit. + """ + _runtime.writer.set_flush_interval(3600) + # Drain anything a previous test left queued, then start from an empty + # directory: the autouse fixture has already pointed base_dir at tmp_path. + failproofai_sdk._writer.flush_now() + events_dir = tmp_path / "events" + if events_dir.exists(): + shutil.rmtree(events_dir) + + def read(): + failproofai_sdk._writer.flush_now() + rows = [] + for path in sorted(events_dir.glob("*.jsonl")): + rows.extend( + json.loads(line) + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() + ) + return rows + + return read + + +@pytest.fixture() +def instrumented(): + _core.set_strict(False) + _compat.set_strict_integrations(False) + _core.reset_failures() + assert failproofai_sdk.instrument("pydantic_ai") == ("pydantic_ai",) + try: + yield + finally: + failproofai_sdk.uninstrument("pydantic_ai") + _core.set_strict(None) + _compat.set_strict_integrations(None) + + +def types_of(rows): + return [row["type"] for row in rows] + + +def of_type(rows, kind): + return [row for row in rows if row["type"] == kind] + + +def one_shot_tool_model(tool_name, args, *, tool_call_id="call-1", text="done"): + """A model that calls one tool, then answers. Deterministic, no network.""" + state = {"n": 0} + + def respond(messages, info): + state["n"] += 1 + if state["n"] == 1: + return ModelResponse(parts=[ToolCallPart(tool_name, args, tool_call_id=tool_call_id)]) + return ModelResponse(parts=[TextPart(text)]) + + return FunctionModel(respond) + + +def weather_agent_with_tool(**kwargs): + agent = Agent( + one_shot_tool_model("get_weather", {"city": "london"}), + name="weather_agent", + **kwargs, + ) + + @agent.tool_plain + def get_weather(city: str) -> str: + return f"sunny in {city}" + + return agent + + +# --------------------------------------------------------------------------- +# The shape of a representative run +# --------------------------------------------------------------------------- + +def test_the_event_type_sequence_for_a_tool_using_run(instrumented, emitted): + result = weather_agent_with_tool().run_sync("weather in london?") + + assert result.output == "done" + assert types_of(emitted()) == [ + "agent_start", + "model_request", + "model_response", + "tool_use", + "tool_result", + "model_request", + "model_response", + "agent_end", + ] + + +def test_the_root_agent_start_is_the_sessions_first_event(instrumented, emitted): + weather_agent_with_tool().run_sync("go") + rows = emitted() + + # `agent_sessions.agent_id = any(...)` over an ORDER BY (session_id, ts) + # table returns the FIRST-by-time agent_id, so anything emitted ahead of the + # root agent_start becomes the name of the whole session in the list view. + assert rows[0]["type"] == "agent_start" + assert rows[0]["agent_id"] == "weather_agent" + assert len({row["session_id"] for row in rows}) == 1 + + +def test_an_async_run_produces_the_same_sequence(instrumented, emitted): + agent = weather_agent_with_tool() + + async def main(): + return await agent.run("go") + + result = asyncio.run(main()) + + assert result.output == "done" + assert types_of(emitted()) == [ + "agent_start", + "model_request", + "model_response", + "tool_use", + "tool_result", + "model_request", + "model_response", + "agent_end", + ] + + +# --------------------------------------------------------------------------- +# Correlation and duration +# --------------------------------------------------------------------------- + +def test_model_events_pair_on_request_id_and_carry_an_int_duration(instrumented, emitted): + weather_agent_with_tool().run_sync("go") + rows = emitted() + + requests = of_type(rows, "model_request") + responses = of_type(rows, "model_response") + assert len(requests) == len(responses) == 2 + + request_ids = [row["request_id"] for row in requests] + assert request_ids == [row["request_id"] for row in responses] + assert len(set(request_ids)) == 2, "request_id must be unique per model call" + + for response in responses: + # `durationOf` prefers the closing event's duration_ms over end-start, + # which is what keeps model durations honest even when the dashboard's + # FIFO pairing brackets the wrong pair. A float silently NULLs the + # promoted u32 column, so the type is the assertion. + assert type(response["duration_ms"]) is int + + +def test_tool_events_pair_on_tool_call_id_and_carry_a_duration(instrumented, emitted): + weather_agent_with_tool().run_sync("go") + rows = emitted() + + (use,) = of_type(rows, "tool_use") + (result,) = of_type(rows, "tool_result") + assert use["tool_call_id"] == result["tool_call_id"] == "call-1" + assert use["tool_name"] == result["tool_name"] == "get_weather" + # Auto-computed by the SDK from the shared tool_call_id: if the adapter ever + # passed two different ids the pairing would break and this would be absent. + assert type(result["duration_ms"]) is int + assert result["output"] == "sunny in london" + + +def test_the_tool_input_is_captured(instrumented, emitted): + weather_agent_with_tool().run_sync("go") + (use,) = of_type(emitted(), "tool_use") + assert use["input"] == {"city": "london"} + + +def test_capture_content_off_drops_payloads_but_keeps_the_structure(emitted): + _core.set_strict(False) + failproofai_sdk.instrument("pydantic_ai", capture_content=False) + try: + weather_agent_with_tool().run_sync("go") + finally: + failproofai_sdk.uninstrument("pydantic_ai") + _core.set_strict(None) + rows = emitted() + + assert types_of(rows) == [ + "agent_start", + "model_request", + "model_response", + "tool_use", + "tool_result", + "model_request", + "model_response", + "agent_end", + ] + assert "goal" not in rows[0] + assert "input" not in of_type(rows, "tool_use")[0] + assert "output" not in of_type(rows, "tool_result")[0] + assert "content" not in of_type(rows, "model_response")[0] + + +def test_usage_is_reported_both_as_ints_and_as_a_normalized_dict(instrumented, emitted): + weather_agent_with_tool().run_sync("go") + rows = emitted() + + response = of_type(rows, "model_response")[0] + assert isinstance(response["input_tokens"], int) + assert isinstance(response["output_tokens"], int) + # Both event_summary.rs and sessionSummary.ts fall back to `usage` when the + # promoted ints are missing, so it ships too. + assert response["usage"]["input_tokens"] == response["input_tokens"] + + (end,) = of_type(rows, "agent_end") + assert end["usage"]["requests"] == 2 + assert end["usage"]["tool_calls"] == 1 + + +# --------------------------------------------------------------------------- +# Identity, labelling, payload hygiene +# --------------------------------------------------------------------------- + +def test_agent_ids_are_readable_names_and_never_uuids(instrumented, emitted): + weather_agent_with_tool().run_sync("go") + rows = emitted() + + ids = {row["agent_id"] for row in rows} + assert ids == {"weather_agent"} + for value in ids: + with pytest.raises(ValueError): + # agent_id is a LowCardinality(String) and the primary facet on every + # dashboard surface; a UUID in it poisons that facet permanently. + uuid.UUID(value) + + +def test_an_unnamed_agent_is_labelled_from_its_variable_not_its_run_id(instrumented, emitted): + forecast_agent = Agent(TestModel(call_tools=[])) + forecast_agent.run_sync("go") + + assert {row["agent_id"] for row in emitted()} == {"forecast_agent"} + + +def test_every_event_carries_the_framework_triple(instrumented, emitted): + weather_agent_with_tool().run_sync("go") + rows = emitted() + + assert rows, "no events were emitted at all" + for row in rows: + assert row["framework"] == "pydantic_ai", row["type"] + assert row["framework_version"], row["type"] + assert row["integration_version"], row["type"] + + +TYPE_OF_DATACLASS = { + re.sub(r"(? str: + raise RuntimeError("kaboom") + + with pytest.raises(RuntimeError): + agent.run_sync("go") + rows = emitted() + + assert types_of(rows) == [ + "agent_start", + "model_request", + "model_response", + "tool_use", + "tool_result", + "error", + "agent_end", + ] + (result,) = of_type(rows, "tool_result") + assert result["error"] == "RuntimeError: kaboom" + + # Exactly one standalone `error` event: the failure escaped the run, so no + # leaf span owns it. Two would double-count in sessionSummary.errorCount. + errors = of_type(rows, "error") + assert len(errors) == 1 + assert errors[0]["error_type"] == "RuntimeError" + assert errors[0]["message"] == "kaboom" + # The traceback is trimmed from the FRONT, so the exception line — the only + # one anybody reads — survives a stack deeper than the 8KB field limit. + assert errors[0]["traceback"].rstrip().endswith("RuntimeError: kaboom") + + # "failed", never "failure" — the server counts only + # error|failed|timeout|rejected as a failure. + (end,) = of_type(rows, "agent_end") + assert end["outcome"] == "failed" + # ...and strictly after the error event: the dashboard closes the agent span + # at agent_end and anything after it is attributed to nothing. + assert types_of(rows).index("error") < types_of(rows).index("agent_end") + + +def test_a_failing_model_request_closes_its_own_span_with_the_error(instrumented, emitted): + def explode(messages, info): + raise RuntimeError("provider is down") + + agent = Agent(FunctionModel(explode), name="model_down_agent") + with pytest.raises(RuntimeError): + agent.run_sync("go") + rows = emitted() + + assert types_of(rows) == [ + "agent_start", + "model_request", + "model_response", + "error", + "agent_end", + ] + (response,) = of_type(rows, "model_response") + # Invariant: a model_request always gets a model_response, even when the call + # blew up — otherwise the leaf never closes and the session reads `ongoing` + # forever. + assert response["error"] == "RuntimeError: provider is down" + assert response["request_id"] == of_type(rows, "model_request")[0]["request_id"] + assert type(response["duration_ms"]) is int + assert len(of_type(rows, "error")) == 1 + + +def test_a_tool_retry_does_not_fail_the_run(instrumented, emitted): + from pydantic_ai import ModelRetry + + calls = {"model": 0, "tool": 0} + + def respond(messages, info): + calls["model"] += 1 + if calls["model"] <= 2: + return ModelResponse( + parts=[ToolCallPart("flaky", {"x": "1"}, tool_call_id=f"call-{calls['model']}")] + ) + return ModelResponse(parts=[TextPart("recovered")]) + + agent = Agent(FunctionModel(respond), name="retry_agent") + + @agent.tool_plain + def flaky(x: str) -> str: + calls["tool"] += 1 + if calls["tool"] == 1: + raise ModelRetry("try again") + return "ok" + + assert agent.run_sync("go").output == "recovered" + rows = emitted() + + # The retry is visible on the tool span that owned it... + results = of_type(rows, "tool_result") + assert results[0]["error"].startswith("ToolRetryError") + assert "error" not in results[1], "the recovered attempt was marked failed" + # ...and nowhere else: the run recovered, so it is not a run-level error. + assert of_type(rows, "error") == [] + assert of_type(rows, "agent_end")[0]["outcome"] == "success" + + +def test_a_cancelled_run_is_not_an_error(instrumented, emitted): + async def main(): + started = asyncio.Event() + agent = Agent( + FunctionModel( + lambda messages, info: ModelResponse( + parts=[ToolCallPart("hang", {}, tool_call_id="call-hang")] + ) + ), + name="cancelled_agent", + ) + + @agent.tool_plain + async def hang() -> str: # pragma: no cover - cancelled before it returns + started.set() + await asyncio.sleep(30) + return "never" + + task = asyncio.create_task(agent.run("go")) + await started.wait() + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + + asyncio.run(main()) + rows = emitted() + + # A cancellation is not a failure: it must not pollute the Errors surface. + assert of_type(rows, "error") == [] + assert of_type(rows, "agent_end")[0]["outcome"] == "cancelled" + # Every open leaf still closes — a run that dies with an open tool_use leaves + # the session `ongoing` forever. The tool runs in its own task and its own + # hook returns *after* `wrap_run` does, so the run closes it on the way out; + # see `test_a_cancelled_leaf_closes_before_the_agent_it_belongs_to`. + assert len(of_type(rows, "tool_use")) == len(of_type(rows, "tool_result")) == 1 + assert of_type(rows, "tool_result")[0].get("error") is None + + +# --------------------------------------------------------------------------- +# A leaf that outlives its own run +# +# Shape A promises the start and the end of a span sit in one frame, and for the +# run itself that holds. It does not hold *between* frames: the graph awaits a +# `gather` of tool tasks, so a cancellation unwinds the run body the moment that +# future is cancelled while each tool task's own `CancelledError` is delivered a +# loop iteration later. Measured against pydantic-ai 2.32 before the fix, a +# `wait_for` timeout put `tool_result` 1ms *after* `agent_end`, and a timeout +# inside the provider call did the same to `model_response` — which is the one +# thing every other emit in the adapter is careful never to do, because the +# dashboard closes the agent span at `agent_end`. +# --------------------------------------------------------------------------- + +def test_a_cancelled_leaf_closes_before_the_agent_it_belongs_to(instrumented, emitted): + async def main(): + started = asyncio.Event() + agent = Agent( + FunctionModel( + lambda messages, info: ModelResponse( + parts=[ToolCallPart("hang", {}, tool_call_id="call-hang")] + ) + ), + name="late_leaf_agent", + ) + + @agent.tool_plain + async def hang() -> str: # pragma: no cover - cancelled before it returns + started.set() + await asyncio.sleep(30) + return "never" + + task = asyncio.create_task(agent.run("go")) + await started.wait() + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + # Give the tool task's own unwind a chance to run: the whole point is + # that it lands after the run, and a duplicate would appear here. + await asyncio.sleep(0.05) + + asyncio.run(main()) + rows = emitted() + kinds = types_of(rows) + + assert kinds.index("tool_result") < kinds.index("agent_end") + # Exactly one, not two: the tool's own hook still runs afterwards and must + # not emit a second close for a span that is already closed. + assert kinds.count("tool_result") == 1 + (result,) = of_type(rows, "tool_result") + assert result["tool_call_id"] == of_type(rows, "tool_use")[0]["tool_call_id"] + # Marked, so a leaf that never reported its own outcome is distinguishable + # from one that completed. + assert result["fw_incomplete"] is True + # A cancellation is still not a failure. + assert result.get("error") is None + assert of_type(rows, "agent_end")[0]["outcome"] == "cancelled" + + +def test_a_cancelled_model_request_closes_before_the_agent_it_belongs_to(instrumented, emitted): + async def main(): + started = asyncio.Event() + + async def never(messages, info): # pragma: no cover - cancelled mid-call + started.set() + await asyncio.sleep(30) + return ModelResponse(parts=[TextPart("never")]) + + agent = Agent(FunctionModel(never), name="late_model_agent") + task = asyncio.create_task(agent.run("go")) + await started.wait() + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + await asyncio.sleep(0.05) + + asyncio.run(main()) + rows = emitted() + kinds = types_of(rows) + + assert kinds.index("model_response") < kinds.index("agent_end") + assert kinds.count("model_response") == 1 + (response,) = of_type(rows, "model_response") + assert response["request_id"] == of_type(rows, "model_request")[0]["request_id"] + assert response["fw_incomplete"] is True + # `duration_ms` is still an int on the synthesized close: the server's JSON + # parser drops floats, so a float silently NULLs the column. + assert type(response["duration_ms"]) is int + assert response.get("error") is None + + +def test_a_streamed_model_response_says_it_was_streamed(instrumented, emitted): + """`duration_ms` on a streamed response is the whole `async with` block. + + Pydantic AI hands the completed `ModelResponse` back only once the caller + leaves `agent.run_stream(...)`, so the consumer's own time is inside the + number — measured against a live gateway, 1.5s of `asyncio.sleep` in the + consumer moved a 3294ms response to 4677ms. No hook closes the span any + earlier, so the flag rides on the response as well as the request: a latency + percentile can exclude these rows instead of averaging UI time into a p95. + """ + + # TestModel, not FunctionModel: only the former can serve a streamed request + # without a hand-written `stream_function`. + agent = Agent(TestModel(), name="streamed_agent") + + async def main(): + async with agent.run_stream("go") as result: + async for _ in result.stream_text(delta=True): + pass + await result.get_output() + + asyncio.run(main()) + rows = emitted() + + responses = of_type(rows, "model_response") + assert responses, types_of(rows) + assert [r.get("fw_streaming") for r in responses] == [True] * len(responses) + assert all(type(r["duration_ms"]) is int for r in responses) + + # ...and a non-streamed run still says so, or the flag means nothing. + agent.run_sync("go") + later = of_type(emitted(), "model_response")[len(responses):] + assert later + assert [r.get("fw_streaming") for r in later] == [False] * len(later) + + +def test_a_completed_leaf_is_never_marked_incomplete(instrumented, emitted): + """The teardown path must not leak into the ordinary one.""" + weather_agent_with_tool().run_sync("weather in london?") + rows = emitted() + + assert [r for r in rows if r.get("fw_incomplete")] == [] + assert types_of(rows).index("tool_result") < types_of(rows).index("agent_end") + + +def test_uninstrumenting_mid_run_closes_the_run_exactly_once(instrumented, emitted): + """`uninstrument()` and the run's own end are both allowed to go first. + + Before the fix they both went: teardown emitted `agent_end` (`cancelled`) + and the run then emitted a second `agent_end` (`success`) against a span the + dashboard had already closed, with the tool's `tool_result` stranded between + them. + """ + + async def main(): + started = asyncio.Event() + release = asyncio.Event() + agent = Agent( + one_shot_tool_model("wait", {}, tool_call_id="call-wait"), + name="torn_down_agent", + ) + + @agent.tool_plain + async def wait() -> str: + started.set() + await release.wait() + return "finished" + + task = asyncio.create_task(agent.run("go")) + await started.wait() + failproofai_sdk.uninstrument("pydantic_ai") + release.set() + await task + + asyncio.run(main()) + rows = emitted() + kinds = types_of(rows) + + assert kinds.count("agent_start") == 1 + assert kinds.count("agent_end") == 1, kinds + assert of_type(rows, "agent_end")[0]["outcome"] == "cancelled" + # ...and the leaf still closed, inside the span it belongs to. + assert kinds.count("tool_result") == 1 + assert kinds.index("tool_result") < kinds.index("agent_end") + + +# --------------------------------------------------------------------------- +# What `tool_result.output` actually says +# --------------------------------------------------------------------------- + +def _tool_returning(value, *, name="produce"): + agent = Agent(one_shot_tool_model(name, {}), name="output_shape_agent") + agent.tool_plain(lambda: value, name=name) + return agent + + +def test_a_tool_returning_a_pydantic_model_is_recorded_as_its_fields(instrumented, emitted): + from pydantic import BaseModel + + class Weather(BaseModel): + city: str + celsius: int + + _tool_returning(Weather(city="Faro", celsius=21)).run_sync("go") + + (result,) = of_type(emitted(), "tool_result") + # Not "Weather(city='Faro', celsius=21)": `truncate` reprs an object with no + # JSON shape, and this one has one. + assert result["output"] == {"city": "Faro", "celsius": 21} + + +def test_a_tool_returning_a_dataclass_is_recorded_as_its_fields(instrumented, emitted): + @dataclasses.dataclass + class Point: + x: int + y: int + + _tool_returning(Point(1, 2)).run_sync("go") + + (result,) = of_type(emitted(), "tool_result") + assert result["output"] == {"x": 1, "y": 2} + + +def test_a_tool_returning_ToolReturn_is_recorded_as_its_return_value(instrumented, emitted): + """`ToolReturn` is an envelope, and the envelope is not the answer. + + `return_value` is what goes back to the model; `metadata` is documented as + never being shown to it at all. Recording the repr of the whole thing buries + the one and publishes the other. + """ + from pydantic_ai.messages import ToolReturn + + _tool_returning( + ToolReturn( + return_value={"answer": 42}, + content="the model sees this", + metadata={"secret": "not for the model"}, + ) + ).run_sync("go") + + (result,) = of_type(emitted(), "tool_result") + assert result["output"] == {"answer": 42} + + +def test_an_ordinary_tool_return_value_is_untouched(instrumented, emitted): + """The unwrapping is narrow: only shapes that have a JSON form.""" + _tool_returning("sunny in london").run_sync("go") + + (result,) = of_type(emitted(), "tool_result") + assert result["output"] == "sunny in london" + + +class _NoJsonShape: + def __repr__(self) -> str: + return "" + + +def test_an_object_with_no_json_shape_is_handed_through_untouched(): + """The unwrapping must not become a second, worse serializer. + + Anything that is not a Pydantic model, a dataclass or a `ToolReturn` comes + back byte-identical, so `_core.truncate` keeps deciding what happens to it — + including the `repr` fallback it documents for an object with no JSON shape. + (Unit-level: pydantic-ai itself refuses to send such a value to a model, so + there is no end-to-end run that reaches this line.) + """ + opaque = _NoJsonShape() + assert adapter._tool_output(opaque) is opaque + assert _core.truncate(adapter._tool_output(opaque)) == "" + + # A class object is not an instance, and `dataclasses.is_dataclass` is True + # for both. + @dataclasses.dataclass + class Shape: + x: int + + assert adapter._tool_output(Shape) is Shape + + +# --------------------------------------------------------------------------- +# Interop with the hand-written API +# --------------------------------------------------------------------------- + +def test_an_enclosing_agenteye_scope_owns_the_session(instrumented, emitted): + with failproofai_sdk.agent("planner", goal="what is the weather?"): + weather_agent_with_tool().run_sync("go") + rows = emitted() + + assert len({row["session_id"] for row in rows}) == 1, "the adapter split the session in two" + assert types_of(rows)[0] == "agent_start" + assert rows[0]["agent_id"] == "planner" + (nested,) = [ + row for row in rows if row["type"] == "agent_start" and row["agent_id"] == "weather_agent" + ] + assert nested["parent_id"] == "planner" + + +def test_a_nested_agent_run_nests(instrumented, emitted): + researcher = Agent(TestModel(call_tools=[]), name="researcher") + supervisor = Agent(one_shot_tool_model("delegate", {"q": "x"}), name="supervisor") + + @supervisor.tool_plain + async def delegate(q: str) -> str: + return (await researcher.run(q)).output + + supervisor.run_sync("go") + rows = emitted() + + assert len({row["session_id"] for row in rows}) == 1 + starts = {row["agent_id"]: row for row in of_type(rows, "agent_start")} + assert set(starts) == {"supervisor", "researcher"} + assert starts["supervisor"].get("parent_id") is None + assert starts["researcher"]["parent_id"] == "supervisor" + # Every event carries the agent_id of an agent whose agent_start is open — + # otherwise the dashboard synthesizes a never-ending root span. + assert {row["agent_id"] for row in rows} == {"supervisor", "researcher"} + + +def test_the_conversation_id_becomes_the_session_id(instrumented, emitted): + agent = Agent(TestModel(call_tools=[]), name="chatty") + agent.run_sync("first", conversation_id="conversation-42") + agent.run_sync("second", conversation_id="conversation-42") + rows = emitted() + + # A conversation spanning several runs is ONE Failproof AI session; two runs are + # two agent spans inside it. + assert {row["session_id"] for row in rows} == {"conversation-42"} + assert len(of_type(rows, "agent_start")) == 2 + + +# --------------------------------------------------------------------------- +# The adapter must never break the host agent +# --------------------------------------------------------------------------- + +class Boom: + """Every attribute is a callable that raises.""" + + def __getattr__(self, name): + def explode(*args, **kwargs): + raise RuntimeError(f"translator exploded in {name}") + + return explode + + +def test_a_translator_that_raises_on_every_call_leaves_the_run_intact( + instrumented, emitted, monkeypatch +): + monkeypatch.setattr(adapter, "_tracker", Boom()) + + result = weather_agent_with_tool().run_sync("weather in london?") + + assert result.output == "done", "the adapter changed what the framework returned" + assert emitted() == [], "a broken translator still managed to emit" + + +def test_a_failing_capability_injection_does_not_break_agent_construction( + instrumented, emitted, monkeypatch +): + def explode(kwargs): + raise RuntimeError("injection exploded") + + monkeypatch.setattr(adapter, "_inject", explode) + + # Construction still succeeds and the run is untouched; it simply records + # nothing, because the capability never got attached. + assert weather_agent_with_tool().run_sync("go").output == "done" + assert emitted() == [] + + +def test_strict_mode_turns_the_swallow_into_a_raise(instrumented, monkeypatch): + """Without this, "it didn't crash" is the only provable property. + + FAILPROOFAI_SDK_STRICT=1 is what makes the never-raise policy testable at all — and + it doubles as the production switch for debugging an adapter gone quiet. + """ + monkeypatch.setattr(adapter, "_tracker", Boom()) + _core.set_strict(True) + try: + with pytest.raises(RuntimeError, match="translator exploded"): + weather_agent_with_tool().run_sync("go") + finally: + _core.set_strict(False) + _core.reset_failures() + + +# --------------------------------------------------------------------------- +# Install / uninstall discipline +# --------------------------------------------------------------------------- + +def test_install_patches_agent_init_and_uninstall_restores_the_saved_object(): + original = Agent.__init__ + failproofai_sdk.instrument("pydantic_ai") + try: + assert Agent.__init__ is not original + assert _core.is_wrapped(Agent.__init__) + # The SAVED object, never a re-import: re-importing to restore hands back + # whatever the attribute's source currently holds, which is how two + # instrumentation libraries silently un-patch each other. + assert _core.unwrap(Agent.__init__) is original + finally: + failproofai_sdk.uninstrument("pydantic_ai") + assert Agent.__init__ is original + + +def test_instrumenting_twice_is_a_no_op(): + failproofai_sdk.instrument("pydantic_ai") + try: + assert failproofai_sdk.instrument("pydantic_ai") == () + finally: + failproofai_sdk.uninstrument("pydantic_ai") + + +def test_an_explicit_capability_is_not_duplicated(instrumented, emitted): + agent = Agent(TestModel(call_tools=[]), name="explicit", capabilities=[FailproofAI()]) + agent.run_sync("go") + + assert types_of(emitted()) == ["agent_start", "model_request", "model_response", "agent_end"] + + +def test_an_agent_built_while_instrumented_goes_inert_after_uninstrument(emitted): + failproofai_sdk.instrument("pydantic_ai") + agent = weather_agent_with_tool() + failproofai_sdk.uninstrument("pydantic_ai") + + # We cannot retro-remove the capability object from an already-built agent, + # so uninstall() makes it a pass-through instead. Anything less means + # uninstrument() does not actually stop the recording. + assert agent.run_sync("go").output == "done" + assert emitted() == [] + + +def test_unknown_options_are_ignored_rather_than_fatal(emitted): + # `failproofai_sdk.instrument()` with no name fans the same **options out to every + # detected adapter, so a keyword meant for LangChain must not take this one + # down. + _core.set_strict(True) + try: + assert failproofai_sdk.instrument("pydantic_ai", langgraph_node_hooks=False) == ("pydantic_ai",) + finally: + failproofai_sdk.uninstrument("pydantic_ai") + _core.set_strict(None) + + +# --------------------------------------------------------------------------- +# Structural anti-drift — the test that catches a silent upstream rename +# --------------------------------------------------------------------------- + +OVERRIDES = { + name: obj + for name, obj in vars(FailproofAI).items() + if inspect.isfunction(obj) and not name.startswith("_") +} + + +class TestAntiDrift: + """Reflection over the REAL base class. + + If upstream renames a callback, our override becomes dead code that is never + called — the framework keeps working, every fake-based test above keeps + passing, and we record nothing. Nothing but reflection catches that. + """ + + def test_the_override_set_is_not_empty(self): + # Guards every test below: `for name in {}` passes vacuously. + assert set(OVERRIDES) == { + "get_ordering", + "wrap_run", + "wrap_model_request", + "wrap_tool_execute", + } + + @pytest.mark.parametrize("name", sorted(OVERRIDES)) + def test_each_override_still_exists_on_the_base_class(self, name): + assert hasattr(AbstractCapability, name), ( + f"FailproofAI.{name} no longer overrides anything on AbstractCapability — " + "it is dead code that will never be called." + ) + + @pytest.mark.parametrize("name", sorted(OVERRIDES)) + def test_each_override_actually_replaces_the_base_implementation(self, name): + assert getattr(AbstractCapability, name) is not OVERRIDES[name] + + @pytest.mark.parametrize("name", sorted(OVERRIDES)) + def test_every_parameter_we_declare_still_exists_on_the_base_signature(self, name): + ours = inspect.signature(OVERRIDES[name]).parameters + theirs = inspect.signature(getattr(AbstractCapability, name)).parameters + for parameter in ours.values(): + if parameter.kind in ( + inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.VAR_KEYWORD, + ): + continue + assert parameter.name in theirs, ( + f"FailproofAI.{name} declares {parameter.name!r}, which " + f"AbstractCapability.{name} no longer accepts" + ) + assert parameter.kind == theirs[parameter.name].kind, ( + f"FailproofAI.{name} takes {parameter.name!r} as {parameter.kind}, " + f"the base now takes it as {theirs[parameter.name].kind}" + ) + + @pytest.mark.parametrize("name", sorted(OVERRIDES)) + def test_every_hook_we_override_is_still_async_where_the_base_is(self, name): + # A `def` where the base has `async def` produces a coroutine the + # framework awaits into a TypeError — or worse, never awaits at all. + assert inspect.iscoroutinefunction(OVERRIDES[name]) == inspect.iscoroutinefunction( + getattr(AbstractCapability, name) + ) + + def test_agent_init_still_takes_a_keyword_only_capabilities_argument(self): + # This is the install mechanism. If `capabilities` stops being a + # keyword-only parameter of Agent.__init__, `install()` silently attaches + # nothing at all. + parameter = inspect.signature(Agent.__init__).parameters["capabilities"] + assert parameter.kind is inspect.Parameter.KEYWORD_ONLY + + def test_there_is_still_no_supported_global_capability_default(self): + # If upstream ever grows a public one, `install()` should stop patching + # `Agent.__init__` and use it — patching a constructor cannot reach + # agents that already exist. + assert not hasattr(Agent, "capabilities_all") + assert not hasattr(Agent, "instrument_all_capabilities") + + @pytest.mark.parametrize( + "attribute", + ["run_id", "conversation_id", "prompt", "agent", "run_step", "metadata", "usage"], + ) + def test_run_context_still_carries_the_fields_we_read(self, attribute): + assert attribute in inspect.get_annotations(RunContext, eval_str=False) + + @pytest.mark.parametrize( + "attribute", ["model", "messages", "model_request_parameters", "streaming", "model_id"] + ) + def test_model_request_context_still_carries_the_fields_we_read(self, attribute): + assert attribute in inspect.get_annotations(ModelRequestContext, eval_str=False) + + @pytest.mark.parametrize("attribute", ["input_tokens", "output_tokens"]) + def test_usage_still_uses_the_2_0_token_names(self, attribute): + # 2.0 renamed request_tokens/response_tokens. Reading the old names would + # report zero tokens on every event, at HTTP 200. + assert hasattr(RunUsage(), attribute) + assert hasattr(RequestUsage(), attribute) + + @pytest.mark.parametrize("attribute", ["tool_name", "tool_call_id", "args"]) + def test_tool_call_part_still_carries_the_fields_we_read(self, attribute): + assert hasattr(ToolCallPart("t", {}, tool_call_id="x"), attribute) + + def test_at_least_one_control_flow_exception_is_still_recognised(self): + # If every name in the list disappeared, `_CONTROL_FLOW` would go empty + # and control flow would start being reported as run failures. + assert adapter._CONTROL_FLOW + + def test_we_deliberately_do_not_override_the_two_hooks_with_side_effects(self): + capability = FailproofAI() + # Overriding wrap_run_event_stream makes `agent.run()` switch itself into + # streaming mode; overriding wrap_node_run flips has_wrap_node_run and + # buys nothing but a doubled row count. + assert capability.has_wrap_run_event_stream is False + assert capability.has_wrap_node_run is False + + def test_the_capability_is_constructible_and_orders_itself_outermost(self): + ordering = FailproofAI().get_ordering() + assert ordering.position == "outermost" diff --git a/sdk/python/tests/test_context.py b/sdk/python/tests/test_context.py new file mode 100644 index 000000000..85596a6e1 --- /dev/null +++ b/sdk/python/tests/test_context.py @@ -0,0 +1,410 @@ +"""Tests for the identity layer: contextvars, the agent stack, and propagate(). + +The tests that matter here are the ones a reviewer cannot derive from reading +the diff: + +* the **adversarially interleaved** task test, which is the only shape that + fails against a module-global implementation; +* the **tuple test**, which is the only shape that fails against + `ContextVar[list]`; +* `propagate` under `pool.map`, which is the only shape that fails against + `partial(copy_context().run, fn)`. +""" + +import asyncio +import threading +from concurrent.futures import ThreadPoolExecutor + +import pytest + +import failproofai_sdk +import failproofai_sdk._context as _context +import failproofai_sdk._runtime as _runtime + + +# --------------------------------------------------------------------------- +# Basic binding +# --------------------------------------------------------------------------- + +def test_nothing_bound_by_default(): + ident = failproofai_sdk.current() + assert ident.session_id is None + assert ident.agent_id is None + assert ident.parent_id is None + assert ident.depth == 0 + + +def test_session_binds_and_unbinds(): + with failproofai_sdk.session("s-1") as sid: + assert sid == "s-1" + assert failproofai_sdk.current().session_id == "s-1" + assert failproofai_sdk.current().session_id is None + + +def test_session_generates_an_id_when_none_bound(): + with failproofai_sdk.session() as sid: + assert isinstance(sid, str) and len(sid) == 32 + assert failproofai_sdk.current().session_id == sid + + +def test_nested_session_inherits_rather_than_generating(events): + with failproofai_sdk.session() as outer: + with failproofai_sdk.session() as inner: + assert inner == outer + with failproofai_sdk.agent("child") as ident: + assert ident.session_id == outer + + +def test_agent_id_falls_back_to_main(): + assert _context.agent_id() == _context.DEFAULT_AGENT_ID == "main" + with failproofai_sdk.session("s-1", agent_id="worker"): + assert _context.agent_id() == "worker" + + +# --------------------------------------------------------------------------- +# Task isolation — must FAIL against a module-global implementation +# --------------------------------------------------------------------------- + +def test_tasks_are_isolated_when_adversarially_interleaved(events): + """Two tasks bind different sessions, then are forced to interleave. + + The `await asyncio.sleep(0)` between binding and emitting is the whole + point: it yields to the event loop *while the scope is open*, so task B + binds its session before task A emits. Against a module global, A's + tool_use would carry B's session_id and this assertion fails. Without the + forced interleave the same test passes on a global and proves nothing. + """ + order: list[str] = [] + + async def run(name: str) -> None: + async with failproofai_sdk.agent(name, session_id=f"sess-{name}"): + order.append(f"bound-{name}") + await asyncio.sleep(0) + await asyncio.sleep(0) + order.append(f"emit-{name}") + _runtime.event.tool_use(tool_name="t", tool_call_id=f"tc-{name}") + await asyncio.sleep(0) + + async def main() -> None: + await asyncio.gather(run("a"), run("b")) + + asyncio.run(main()) + + # Prove the interleave actually happened; otherwise the test is vacuous. + assert order.index("bound-b") < order.index("emit-a") + + by_call = {e["tool_call_id"]: e for e in events.entries if e["type"] == "tool_use"} + assert by_call["tc-a"]["session_id"] == "sess-a" + assert by_call["tc-a"]["agent_id"] == "a" + assert by_call["tc-b"]["session_id"] == "sess-b" + assert by_call["tc-b"]["agent_id"] == "b" + + +def test_agent_stack_is_a_tuple_not_a_shared_list(events): + """Task A's stack must be unmodified by task B pushing a nested agent. + + A `ContextVar[list]` holds the *same list object* in both tasks, so B's + push would show up in A's view. This is the test that catches it. + """ + seen: dict[str, tuple[str, ...]] = {} + pushed = asyncio.Event() + + async def first() -> None: + async with failproofai_sdk.agent("root-a", session_id="s"): + async with failproofai_sdk.agent("child-a"): + # Hand control to the other task, which pushes child-b. + await pushed.wait() + seen["a"] = _context._AGENT_STACK.get() + + async def second() -> None: + async with failproofai_sdk.agent("root-b", session_id="s"): + async with failproofai_sdk.agent("child-b"): + seen["b"] = _context._AGENT_STACK.get() + pushed.set() + await asyncio.sleep(0) + + async def main() -> None: + await asyncio.gather(first(), second()) + + asyncio.run(main()) + + assert seen["a"] == ("root-a", "child-a") + assert seen["b"] == ("root-b", "child-b") + assert isinstance(seen["a"], tuple) + + +def test_create_task_inherits_without_propagate(events): + """Pinned deliberately: contextvars DO flow into asyncio tasks. + + Nobody should later sprinkle `propagate()` over async code — a Task copies + the current context at creation, so the identity is already there. + """ + captured: list = [] + + async def child() -> None: + captured.append(failproofai_sdk.current()) + + async def main() -> None: + with failproofai_sdk.agent("parent", session_id="s-async"): + await asyncio.create_task(child()) + + asyncio.run(main()) + assert captured[0].session_id == "s-async" + assert captured[0].agent_id == "parent" + + +# --------------------------------------------------------------------------- +# Thread isolation and propagate() +# --------------------------------------------------------------------------- + +def test_threads_do_not_inherit_identity(): + seen: list = [] + + def worker() -> None: + seen.append(failproofai_sdk.current()) + + with failproofai_sdk.session("s-thread"): + t = threading.Thread(target=worker) + t.start() + t.join() + + assert seen[0].session_id is None + + +def test_bare_submit_inside_agent_raises_naming_propagate(events): + def worker(): + _runtime.event.tool_use(tool_name="t", tool_call_id="tc") + + with failproofai_sdk.agent("a", session_id="s"): + with ThreadPoolExecutor(max_workers=1) as pool: + future = pool.submit(worker) + with pytest.raises(TypeError) as excinfo: + future.result() + + assert "propagate" in str(excinfo.value) + + +def test_propagate_with_pool_submit(events): + def worker(n): + _runtime.event.tool_use(tool_name="t", tool_call_id=f"tc-{n}") + return failproofai_sdk.current().session_id + + with failproofai_sdk.agent("a", session_id="s-submit"): + with ThreadPoolExecutor(max_workers=2) as pool: + results = [pool.submit(failproofai_sdk.propagate(worker), n).result() for n in range(3)] + + assert results == ["s-submit"] * 3 + tool_uses = [e for e in events.entries if e["type"] == "tool_use"] + assert {e["session_id"] for e in tool_uses} == {"s-submit"} + assert {e["agent_id"] for e in tool_uses} == {"a"} + + +def test_propagate_survives_reuse_via_pool_map(events): + """`pool.map` calls ONE wrapper object many times, concurrently. + + This is what `partial(contextvars.copy_context().run, fn)` cannot do: a + `Context` raises `RuntimeError: cannot enter context ... already entered` + on the second concurrent entry, and mutations inside it persist into the + next call. Snapshot/restore has neither problem. + """ + # Forces four workers to be *inside* the wrapper simultaneously. Without + # it the calls serialise and a copy_context() implementation passes by + # accident, which makes the test worthless. + barrier = threading.Barrier(4) + + def worker(n): + ident = failproofai_sdk.current() + barrier.wait(timeout=5) + # Mutate the stack, to prove the mutation does not leak into the next + # invocation of the same wrapper. + with failproofai_sdk.agent(f"child-{n}", session_id=ident.session_id): + pass + return (ident.session_id, ident.agent_id, ident.depth) + + with failproofai_sdk.agent("a", session_id="s-map"): + wrapped = failproofai_sdk.propagate(worker) + with ThreadPoolExecutor(max_workers=4) as pool: + results = list(pool.map(wrapped, range(8))) + + assert results == [("s-map", "a", 1)] * 8 + + +def test_propagate_with_threading_thread(events): + seen: list = [] + + def worker(): + seen.append(failproofai_sdk.current()) + + with failproofai_sdk.agent("a", session_id="s-thread2"): + t = threading.Thread(target=failproofai_sdk.propagate(worker)) + t.start() + t.join() + + assert seen[0].session_id == "s-thread2" + assert seen[0].agent_id == "a" + + +def test_propagate_with_run_in_executor(events): + def worker(): + return failproofai_sdk.current().session_id + + async def main(): + with failproofai_sdk.agent("a", session_id="s-executor"): + loop = asyncio.get_running_loop() + with ThreadPoolExecutor(max_workers=1) as pool: + return await loop.run_in_executor(pool, failproofai_sdk.propagate(worker)) + + assert asyncio.run(main()) == "s-executor" + + +def test_propagate_does_not_leak_into_the_worker_thread_afterwards(): + seen: list = [] + + def worker(): + pass + + def after(): + seen.append(failproofai_sdk.current().session_id) + + with failproofai_sdk.session("s-leak"): + wrapped = failproofai_sdk.propagate(worker) + with ThreadPoolExecutor(max_workers=1) as pool: + pool.submit(wrapped).result() + pool.submit(after).result() + + assert seen[0] is None + + +def test_propagate_preserves_metadata(): + def worker(a, b=2): + """docstring.""" + return a + b + + wrapped = failproofai_sdk.propagate(worker) + assert wrapped.__name__ == "worker" + assert wrapped.__doc__ == "docstring." + assert wrapped(1, b=3) == 4 + + +# --------------------------------------------------------------------------- +# Explicit identity always wins +# --------------------------------------------------------------------------- + +def test_explicit_ids_override_the_context(events): + with failproofai_sdk.agent("ctx-agent", session_id="ctx-session"): + _runtime.event.tool_use( + session_id="explicit-session", + agent_id="explicit-agent", + tool_name="t", + tool_call_id="tc", + ) + entry = [e for e in events.entries if e["type"] == "tool_use"][0] + assert entry["session_id"] == "explicit-session" + assert entry["agent_id"] == "explicit-agent" + + +def test_explicit_agent_id_only_keeps_context_session(events): + with failproofai_sdk.agent("ctx-agent", session_id="ctx-session"): + _runtime.event.tool_use(agent_id="other", tool_name="t", tool_call_id="tc") + entry = [e for e in events.entries if e["type"] == "tool_use"][0] + assert entry["session_id"] == "ctx-session" + assert entry["agent_id"] == "other" + + +def test_missing_session_outside_any_scope_raises(events): + with pytest.raises(TypeError, match="propagate"): + _runtime.event.agent_start() + + +def test_agent_id_defaults_to_main_with_only_a_session(events): + with failproofai_sdk.session("s-only"): + _runtime.event.agent_start() + assert events.last()["agent_id"] == "main" + + +# --------------------------------------------------------------------------- +# Exception precedence is unchanged by the identity fallback +# --------------------------------------------------------------------------- + +def test_duration_ms_guard_still_beats_the_identity_error(events): + """No session bound, and duration_ms passed. The ValueError must win. + + `_identity()` runs after the guard precisely so today's precedence holds. + """ + with pytest.raises(ValueError, match="duration_ms"): + _runtime.event.tool_result(tool_name="t", tool_call_id="tc", duration_ms=99) + + +def test_reserved_field_error_still_beats_the_identity_error(events): + with pytest.raises(ValueError, match="Reserved field"): + _runtime.event.agent_start(timestamp="nope") + + +def test_duplicate_session_id_still_raises_type_error(events): + with pytest.raises(TypeError): + _runtime.event.agent_start(session_id="s", **{"session_id": "s2"}) + + +# --------------------------------------------------------------------------- +# `_pending` correlation keys +# --------------------------------------------------------------------------- + +def test_hook_id_no_longer_collides_with_tool_call_id(events): + ns = _runtime.event + ns.tool_use(session_id="s", agent_id="a", tool_name="t", tool_call_id="x1") + ns.hook_completed(session_id="s", agent_id="a", hook_name="h", hook_id="x1") + assert "duration_ms" not in events.last() + # And the tool's own pending entry is untouched, so it still pairs. + ns.tool_result(session_id="s", agent_id="a", tool_name="t", tool_call_id="x1") + assert "duration_ms" in events.last() + + +def test_hook_pair_still_produces_a_duration(events): + ns = _runtime.event + ns.hook_triggered(session_id="s", agent_id="a", hook_name="h", hook_id="h1") + ns.hook_completed(session_id="s", agent_id="a", hook_name="h", hook_id="h1") + assert "duration_ms" in events.last() + + +def test_tool_keys_stay_bare_so_cross_agent_pairs_still_match(events): + """A tool started under one agent and finished under another must pair. + + Namespacing tool keys by agent_id would silently turn this correct + duration_ms into a missing one, and this is routine once frameworks run + tools inside sub-agents. + """ + ns = _runtime.event + ns.tool_use(session_id="s", agent_id="planner", tool_name="t", tool_call_id="tc") + ns.tool_result(session_id="s", agent_id="worker", tool_name="t", tool_call_id="tc") + assert "duration_ms" in events.last() + + +def test_track_pending_is_thread_safe_at_the_cap(events): + """Hammer `_track_pending` at the cap from several threads. + + Unlocked, the read-modify-write lets two threads pick the same `oldest` and + the second `del` raises KeyError *inside* the caller's event.tool_use(). + """ + from failproofai_sdk._events import _PENDING_CAP + + ns = _runtime.event + for i in range(_PENDING_CAP): + ns._track_pending(f"seed-{i}", ns._now()) + + errors: list[Exception] = [] + + def hammer(worker: int) -> None: + try: + for i in range(200): + ns._track_pending(f"w{worker}-{i}", ns._now()) + except Exception as exc: + errors.append(exc) + + threads = [threading.Thread(target=hammer, args=(w,)) for w in range(8)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert errors == [] + assert len(ns._pending) == _PENDING_CAP diff --git a/sdk/python/tests/test_docs.py b/sdk/python/tests/test_docs.py new file mode 100644 index 000000000..4fbd936a8 --- /dev/null +++ b/sdk/python/tests/test_docs.py @@ -0,0 +1,411 @@ +"""`docs/` is the integration surface, so it is code under test. + +The guide and the runnable code live together — one directory per framework, +each holding the README somebody reads and the `examples/` they run: + + docs/ + README.md the index + _shared/ cosmetics for the examples; never part of the SDK + manual/ no framework, raw scopes + event.* + langgraph/ crewai/ llama_index/ pydantic_ai/ + README.md the guide + examples/*.py runnable, and run before shipping + +Nothing here checks prose. It checks the three ways a docs tree rots without +anybody noticing: a framework gains an adapter and never gains a page, a page or +an example names an API that no longer exists, and an example drifts into +teaching the manual identity path the scopes replaced. + +The examples themselves need a framework and an API key, so they cannot run in +unit CI — which is exactly why they need a guard that does not. +""" +from __future__ import annotations + +import ast +import re +import textwrap +from pathlib import Path + +import pytest + +import failproofai_sdk + +DOCS = Path(failproofai_sdk.__file__).resolve().parent.parent / "docs" + +#: adapter registry name -> the directory that documents it. +#: `langgraph` is the directory for the `langchain` adapter: it is the spelling +#: people search for, and the adapter serves both. +ADAPTER_DIRS = { + "langchain": "langgraph", + "crewai": "crewai", + "llama_index": "llama_index", + "pydantic_ai": "pydantic_ai", +} + +#: directory -> the failproofai-sdk extra its guide must name. +DIR_EXTRA = { + "langgraph": "langgraph", + "crewai": "crewai", + "llama_index": "llamaindex", + "pydantic_ai": "pydantic-ai", +} + +#: directory -> third-party modules its examples may import. `manual/` maps to +#: the openai client and nothing else: it is the "no framework" path, so +#: borrowing one would defeat the point of it. +DIR_IMPORTS = { + "langgraph": {"langchain_core", "langgraph", "langchain_openai"}, + "crewai": {"crewai"}, + "llama_index": {"llama_index"}, + "pydantic_ai": {"pydantic_ai", "pydantic"}, + "manual": {"openai"}, +} + +#: stdlib and local helpers every example may import. `_shared` is the trace +#: printer and is explicitly not part of the SDK surface. +ALWAYS_ALLOWED = {"failproofai_sdk", "_shared", "os", "sys", "json", "asyncio", "pathlib"} + +ALL_DIRS = sorted(DIR_IMPORTS) + + +def _guides() -> list[Path]: + """The framework guides. `_shared/README.md` documents the trace printer, + which is cosmetics rather than an integration, so it is not one of these.""" + return sorted((DOCS / d / "README.md") for d in ALL_DIRS) + + +def _examples() -> list[Path]: + return sorted(p for d in ALL_DIRS for p in (DOCS / d / "examples").glob("*.py")) + + +EXAMPLES = _examples() +EXAMPLE_IDS = [f"{p.parents[1].name}/{p.name}" for p in EXAMPLES] + + +def _imports(tree: ast.AST) -> set[str]: + out: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + out |= {a.name.split(".")[0] for a in node.names} + elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: + out.add(node.module.split(".")[0]) + return out + + +# ───────────────────────────────────────────────────────────────────────────── +# Shape +# ───────────────────────────────────────────────────────────────────────────── + + +def test_the_docs_tree_exists(): + assert DOCS.is_dir(), f"{DOCS} is missing" + assert (DOCS / "README.md").is_file(), "docs/README.md — the index — is missing" + + +def test_there_is_a_directory_for_every_adapter(): + """An adapter with no guide is one nobody will find.""" + from failproofai_sdk.integrations import _REGISTRY + + assert set(ADAPTER_DIRS) == set(_REGISTRY), ( + f"adapters {sorted(_REGISTRY)} but guides for {sorted(ADAPTER_DIRS)}" + ) + for adapter, directory in ADAPTER_DIRS.items(): + assert (DOCS / directory).is_dir(), f"{adapter} has no docs/{directory}/" + + +def test_every_documented_directory_is_accounted_for(): + """A directory nobody links to is a directory nobody maintains.""" + found = {p.name for p in DOCS.iterdir() if p.is_dir() and p.name != "_shared"} + assert found == set(ALL_DIRS) + + +@pytest.mark.parametrize("directory", ALL_DIRS) +def test_a_directory_has_a_guide_and_examples(directory): + assert (DOCS / directory / "README.md").is_file(), f"{directory}/README.md missing" + examples = list((DOCS / directory / "examples").glob("*.py")) + assert examples, f"{directory}/examples/ has no runnable code" + + +@pytest.mark.parametrize("directory", ALL_DIRS) +def test_a_directory_has_a_quickstart(directory): + """`quickstart.py` is the fixed entry point every guide links to.""" + assert (DOCS / directory / "examples" / "quickstart.py").is_file() + + +@pytest.mark.parametrize("directory", ALL_DIRS) +def test_a_directory_has_more_than_a_quickstart(directory): + """A quickstart alone never shows a multi-event flow, which is the point.""" + assert len(list((DOCS / directory / "examples").glob("*.py"))) >= 2 + + +def test_the_index_links_to_every_directory(): + index = (DOCS / "README.md").read_text(encoding="utf-8") + for directory in ALL_DIRS: + assert f"{directory}/" in index, f"docs/README.md never links to {directory}/" + + +# ───────────────────────────────────────────────────────────────────────────── +# The guides +# ───────────────────────────────────────────────────────────────────────────── + + +@pytest.mark.parametrize("guide", _guides(), ids=lambda p: p.parent.name) +def test_a_guide_only_names_api_this_package_exports(guide): + """A rename in the SDK must break this test, not somebody's copy-paste.""" + text = guide.read_text(encoding="utf-8") + referenced = set(re.findall(r"failproofai_sdk\.([a-z_]+)\s*\(", text)) - {"event"} + missing = {name for name in referenced if not hasattr(failproofai_sdk, name)} + assert not missing, f"{guide.parent.name} names failproofai_sdk.{missing}, which does not exist" + + +@pytest.mark.parametrize("guide", _guides(), ids=lambda p: p.parent.name) +def test_a_guide_only_names_event_methods_that_exist(guide): + text = guide.read_text(encoding="utf-8") + referenced = set(re.findall(r"failproofai_sdk\.event\.([a-z_]+)\s*\(", text)) + missing = {n for n in referenced if not hasattr(failproofai_sdk.event, n)} + assert not missing, f"{guide.parent.name} names event.{missing}, which does not exist" + + +@pytest.mark.parametrize("guide", _guides() + [DOCS / "README.md"], ids=lambda p: str(p.parent.name or "index")) +def test_a_guide_only_names_extras_that_exist(guide): + """A wrong extra in an install line is a five-minute dead end.""" + try: # Python 3.11+ + import tomllib + except ModuleNotFoundError: + import tomli as tomllib + + manifest = tomllib.loads((DOCS.parent / "pyproject.toml").read_text(encoding="utf-8")) + extras = set(manifest["project"]["optional-dependencies"]) + named = set(re.findall(r"failproofai-sdk\[([a-z-]+)\]", guide.read_text(encoding="utf-8"))) + assert named <= extras, f"{guide} names non-existent extras: {sorted(named - extras)}" + + +@pytest.mark.parametrize("directory,extra", sorted(DIR_EXTRA.items())) +def test_a_framework_guide_names_its_own_extra(directory, extra): + text = (DOCS / directory / "README.md").read_text(encoding="utf-8") + assert "pip install" in text, f"{directory} never says how to install it" + assert f"failproofai-sdk[{extra}]" in text, f"{directory} does not name the {extra!r} extra" + + +@pytest.mark.parametrize("directory", sorted(DIR_EXTRA)) +def test_a_framework_guide_shows_instrument_and_a_session(directory): + text = (DOCS / directory / "README.md").read_text(encoding="utf-8") + assert "failproofai_sdk.instrument(" in text, f"{directory} never shows instrument()" + assert "failproofai_sdk.session()" in text, f"{directory} never shows a session" + + +@pytest.mark.parametrize("guide", _guides(), ids=lambda p: p.parent.name) +def test_a_guide_links_to_the_examples_it_documents(guide): + """A guide describing a file that does not exist is worse than no guide.""" + text = guide.read_text(encoding="utf-8") + linked = set(re.findall(r"\(examples/([\w.]+\.py)\)", text)) + on_disk = {p.name for p in (guide.parent / "examples").glob("*.py")} + assert linked, f"{guide.parent.name} links to none of its examples" + assert linked <= on_disk, f"{guide.parent.name} links to missing {sorted(linked - on_disk)}" + + +def test_the_manual_guide_does_not_teach_instrument(): + """`manual/` is the no-adapter path; showing instrument() there is a slip. + + Scoped to the part of the page that teaches the manual API — the section on + unsupported frameworks legitimately contrasts the two. + """ + text = (DOCS / "manual" / "README.md").read_text(encoding="utf-8") + body = text.split("## Instrumenting an unsupported framework")[0] + assert "failproofai_sdk.instrument()" not in body + + +def test_the_index_lists_every_event_type(): + """The event matrix is the index's whole value; a missing row is a silent gap.""" + from failproofai_sdk._events import EventNamespace + + text = (DOCS / "README.md").read_text(encoding="utf-8") + methods = [ + n for n in vars(EventNamespace) + if not n.startswith("_") and callable(getattr(EventNamespace, n)) + ] + missing = [m for m in methods if m not in text] + assert not missing, f"docs/README.md does not mention {missing}" + + +# ───────────────────────────────────────────────────────────────────────────── +# The examples +# ───────────────────────────────────────────────────────────────────────────── + + +@pytest.mark.parametrize("path", EXAMPLES, ids=EXAMPLE_IDS) +def test_an_example_parses(path): + ast.parse(path.read_text(encoding="utf-8")) + + +@pytest.mark.parametrize("path", EXAMPLES, ids=EXAMPLE_IDS) +def test_an_example_imports_only_its_own_framework(path): + """Borrowing a second framework turns an example into an install problem.""" + tree = ast.parse(path.read_text(encoding="utf-8")) + allowed = ALWAYS_ALLOWED | DIR_IMPORTS[path.parents[1].name] + unexpected = _imports(tree) - allowed + assert not unexpected, f"{path.parents[1].name}/{path.name} imports {sorted(unexpected)}" + + +@pytest.mark.parametrize("path", EXAMPLES, ids=EXAMPLE_IDS) +def test_an_example_calls_api_this_package_exports(path): + called = { + node.func.attr + for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "failproofai_sdk" + } + assert called, f"{path.name} never calls failproofai_sdk.*" + missing = {c for c in called if not hasattr(failproofai_sdk, c)} + assert not missing, f"{path.name} calls failproofai_sdk.{missing} which does not exist" + + +@pytest.mark.parametrize("path", EXAMPLES, ids=EXAMPLE_IDS) +def test_an_example_does_not_thread_identity_by_hand(path): + """`session_id=` everywhere teaches the path the scopes were built to remove. + + Checked over the AST rather than the raw text, so an example is free to + *explain* `session_id=` in its docstring — which the manual quickstart has + to, since explaining what the scopes replace is its whole job. + """ + threaded = [ + node + for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))) + if isinstance(node, ast.Call) + for kw in node.keywords + if kw.arg in ("session_id", "agent_id") + ] + assert not threaded, ( + f"{path.parents[1].name}/{path.name} passes session_id/agent_id by hand" + ) + + +@pytest.mark.parametrize("path", EXAMPLES, ids=EXAMPLE_IDS) +def test_an_example_opens_a_session(path): + source = path.read_text(encoding="utf-8") + assert "failproofai_sdk.session()" in source, f"{path.name} never opens a session" + + +@pytest.mark.parametrize( + "path", + [p for p in EXAMPLES if p.parents[1].name != "manual"], + ids=[i for i in EXAMPLE_IDS if not i.startswith("manual/")], +) +def test_a_framework_example_instruments(path): + source = path.read_text(encoding="utf-8") + assert "failproofai_sdk.instrument()" in source, f"{path.name} never instruments" + + +@pytest.mark.parametrize( + "path", + [p for p in EXAMPLES if p.parents[1].name == "manual"], + ids=[i for i in EXAMPLE_IDS if i.startswith("manual/")], +) +def test_a_manual_example_does_not_instrument(path): + """`manual/` exists to show the path with no adapter.""" + source = path.read_text(encoding="utf-8") + assert "failproofai_sdk.instrument()" not in source + + +@pytest.mark.parametrize("path", EXAMPLES, ids=EXAMPLE_IDS) +def test_an_example_documents_how_to_run_itself(path): + """The docstring is what somebody reads before running it.""" + tree = ast.parse(path.read_text(encoding="utf-8")) + doc = ast.get_docstring(tree) or "" + assert "pip install" in doc, f"{path.name} does not say how to install it" + expected = f"python docs/{path.parents[1].name}/examples/{path.name}" + assert expected in doc, f"{path.name} does not show `{expected}`" + + +def test_the_shared_helper_is_not_importable_from_the_sdk(): + """`_shared` is cosmetics. If it ever became a dependency of the package, + the zero-dependency promise would be quietly routed around.""" + import failproofai_sdk.integrations as integrations + + for module in (failproofai_sdk, integrations): + source = Path(module.__file__).read_text(encoding="utf-8") + assert "_shared" not in source + + +# ───────────────────────────────────────────────────────────────────────────── +# Documented options must exist +# ───────────────────────────────────────────────────────────────────────────── +# +# The guides shipped naming `capture_content` for CrewAI and `session_id` for +# LlamaIndex. Neither adapter reads either one, so both were silently ignored: +# `instrument()` passes the same dict to every adapter and unknown keys are +# dropped by design, which means a reader following the docs got no error and no +# effect. Nothing checked, because the option lists were prose. + +#: docs directory -> the adapter module that backs it. +DIR_ADAPTER = { + "langgraph": "langchain", + "crewai": "crewai", + "llama_index": "llama_index", + "pydantic_ai": "pydantic_ai", +} + + +def _adapter_options(module_name: str) -> set[str]: + """Every option key an adapter actually reads, from its source. + + Two spellings, because the adapters use two: `options.get("x")` for the ones + that read the dict directly, and a dataclass of defaults for LangChain, + which parses the dict into `_Options` first. + """ + import failproofai_sdk + + source = ( + Path(failproofai_sdk.__file__).resolve().parent + / "integrations" + / f"{module_name}.py" + ).read_text(encoding="utf-8") + + names = set(re.findall(r'options\.get\(\s*["\'](\w+)["\']', source)) + + tree = ast.parse(source) + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == "_Options": + names |= { + stmt.target.id + for stmt in node.body + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name) + } + return names + + +def _documented_options(text: str, adapter_name: str) -> set[str]: + """Keyword names inside a documented `instrument("", ...)` call.""" + documented: set[str] = set() + for block in re.findall(r"```python[^\n]*\n(.*?)```", text, re.S): + try: + tree = ast.parse(textwrap.dedent(block)) + except SyntaxError: # a fragment, not a whole program + continue + for node in ast.walk(tree): + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)): + continue + if node.func.attr != "instrument": + continue + first = node.args[0] if node.args else None + if isinstance(first, ast.Constant) and first.value == adapter_name: + documented |= {kw.arg for kw in node.keywords if kw.arg} + return documented + + +@pytest.mark.parametrize("directory,module_name", sorted(DIR_ADAPTER.items())) +def test_a_guide_only_documents_options_the_adapter_reads(directory, module_name): + text = (DOCS / directory / "README.md").read_text(encoding="utf-8") + documented = _documented_options(text, module_name) + if not documented: + pytest.skip(f"{directory} documents no instrument() options") + real = _adapter_options(module_name) + invented = documented - real + assert not invented, ( + f"{directory}/README.md documents instrument({module_name!r}, ...) options " + f"{sorted(invented)} that the adapter never reads. Unknown keys are " + f"dropped silently, so a reader following this gets no error and no effect. " + f"Real options: {sorted(real)}" + ) diff --git a/sdk/python/tests/test_durability.py b/sdk/python/tests/test_durability.py index 8ea96ed90..0bc03655d 100644 --- a/sdk/python/tests/test_durability.py +++ b/sdk/python/tests/test_durability.py @@ -458,8 +458,8 @@ def test_pending_map_is_capped_and_evicts_oldest_first(): namespace.tool_use(session_id="s", agent_id="a", tool_name="t", tool_call_id=f"c{i}") assert len(namespace._pending) == _PENDING_CAP - assert _tool_key("s", "a", "c0") not in namespace._pending, "eviction is not FIFO" - assert _tool_key("s", "a", f"c{_PENDING_CAP + 99}") in namespace._pending + assert _tool_key("s", "c0") not in namespace._pending, "eviction is not FIFO" + assert _tool_key("s", f"c{_PENDING_CAP + 99}") in namespace._pending def test_an_evicted_pair_completes_without_duration_instead_of_raising(): @@ -907,19 +907,43 @@ def test_hook_pairs_are_namespaced_by_session_and_agent(): assert "duration_ms" in writer.entries[0], f"{session} lost its own start" -def test_the_same_id_in_the_same_session_but_a_different_agent_does_not_collide(): - """Both halves of the namespace are load-bearing, not just the session.""" +def test_a_pair_opened_and_closed_under_different_agents_still_pairs(): + """The key is deliberately NOT agent-scoped, and this is why. + + This test asserted the opposite when the keys were first namespaced: that a + tool id repeated under two agents in one session produced two independent + pairs. That looked like tightening; it was over-tightening. Once a framework + runs tools inside sub-agents — LangGraph and CrewAI both do — a `tool_use` + opened under `planner` and closed under `worker` is the ORDINARY case, and an + agent-scoped key makes it miss silently, dropping `duration_ms` for exactly + the nested runs that most need it. + + The rule that survives both: key on what makes the id unique (kind, session) + and never on what can legitimately change between the two events (the agent). + """ writer = _NullWriter() namespace = EventNamespace(writer) namespace.tool_use(session_id="S", agent_id="planner", tool_name="t", tool_call_id="x") - namespace.tool_use(session_id="S", agent_id="worker", tool_name="t", tool_call_id="x") - assert len(namespace._pending) == 2 + writer.entries.clear() + namespace.tool_result(session_id="S", agent_id="worker", tool_name="t", tool_call_id="x") + assert "duration_ms" in writer.entries[0], ( + "a tool handed from planner to worker lost its duration — the key is " + "agent-scoped again" + ) + assert namespace._pending == {}, "the pending entry was left behind" + + +def test_a_hook_opened_and_closed_under_different_agents_still_pairs(): + """Same rule, same reason, for the other adapter-driven pair type.""" + writer = _NullWriter() + namespace = EventNamespace(writer) + + namespace.hook_triggered(session_id="S", agent_id="planner", hook_name="h", hook_id="x") writer.entries.clear() - namespace.tool_result(session_id="S", agent_id="planner", tool_name="t", tool_call_id="x") + namespace.hook_completed(session_id="S", agent_id="worker", hook_name="h", hook_id="x") assert "duration_ms" in writer.entries[0] - assert len(namespace._pending) == 1, "the worker's pending start was consumed too" def test_a_result_from_an_unrelated_session_gets_no_duration_at_all(): @@ -1120,3 +1144,85 @@ def test_a_persistent_write_fault_does_not_strand_a_tmp_file_per_cycle(spool, mo writer.flush_now() assert len(read_all(spool)) == 50, "the recovered batch is incomplete" assert list((spool / "events").glob("*.tmp")) == [] + + +# ───────────────────────────────────────────────────────────────────────────── +# SIGTERM — the exit path the docs got wrong +# ───────────────────────────────────────────────────────────────────────────── + + +def _run_child(tmp_path: Path, body: str) -> subprocess.CompletedProcess: + """Run `body` in a fresh interpreter spooling into `tmp_path`. + + A subprocess rather than a fork: the point is CPython's *default* signal + disposition in a process this test did not otherwise touch, and pytest's own + handlers are inherited across a fork. + """ + src = ( + "import os, signal, sys\n" + "import failproofai_sdk as fp\n" + "from failproofai_sdk import event\n" + f"fp.configure(base_dir={str(tmp_path)!r}, flush_interval=3600.0)\n" + body + ) + return subprocess.run( + [sys.executable, "-c", src], + capture_output=True, + text=True, + timeout=60, + cwd=str(Path(__file__).resolve().parents[1]), + ) + + +def test_sigterm_drops_the_queue_because_cpython_runs_no_atexit_for_it(tmp_path): + """The claim this replaces said `atexit` *does* run on `SIGTERM`. It does not. + + CPython installs no handler for `SIGTERM` — `signal.getsignal(SIGTERM)` is + `SIG_DFL` — so the OS terminates the process where it stands and the atexit + flush never runs. `SKILL.md` told readers the opposite, under a heading + naming rolling deploys and `docker stop`, which is exactly the population + that would have believed it and shipped nothing. + + The long flush interval isolates the exit path: in real use the 0.5s default + is what bounds the loss, and that bound is the whole mitigation. + """ + proc = _run_child( + tmp_path, + "with fp.session('sigterm-bare'):\n" + " for i in range(20):\n" + " event.agent_start(agent_id='a', goal=str(i))\n" + "os.kill(os.getpid(), signal.SIGTERM)\n", + ) + assert proc.returncode == -15, proc.stderr + assert read_all(tmp_path) == [], ( + "SIGTERM must be shown losing the queue; if this now passes events " + "through, the SDK grew a handler and SKILL.md's recipe is obsolete" + ) + + +def test_the_documented_sigterm_handler_saves_the_queue_and_closes_the_run(tmp_path): + """The recipe SKILL.md now ships, executed rather than described. + + `sys.exit` and not `os._exit`: it unwinds, so an open `agent()` scope emits + its `agent_end` before the flush — the events most likely to be in flight at + shutdown are exactly the ones that close a run. + """ + proc = _run_child( + tmp_path, + "def _flush_and_exit(signum, frame):\n" + " fp._writer.flush_now()\n" + " sys.exit(128 + signum)\n" + "signal.signal(signal.SIGTERM, _flush_and_exit)\n" + "with fp.agent('worker', session_id='sigterm-handled', goal='survive'):\n" + " for i in range(20):\n" + " event.tool_use(tool_name='t', tool_call_id=str(i), input={'i': i})\n" + " os.kill(os.getpid(), signal.SIGTERM)\n", + ) + assert proc.returncode == 128 + 15, proc.stderr + events = read_all(tmp_path) + kinds = {e["type"] for e in events} + assert len([e for e in events if e["type"] == "tool_use"]) == 20 + assert "agent_start" in kinds and "agent_end" in kinds + # The interrupted run closes as failed, carrying the SystemExit — an evicted + # run did not finish, and that is the thing an operator needs to see. + end = next(e for e in events if e["type"] == "agent_end") + assert end["outcome"] == "failed" diff --git a/sdk/python/tests/test_integrations.py b/sdk/python/tests/test_integrations.py new file mode 100644 index 000000000..5aac5f349 --- /dev/null +++ b/sdk/python/tests/test_integrations.py @@ -0,0 +1,998 @@ +"""The integrations scaffolding, exercised without any framework installed. + +Everything here runs against a **fake** adapter registered into the real +registry, and `sys.modules` stubs for the patch mechanics. That is deliberate: +the registry, the failure policy and the install/uninstall discipline are the +parts that must be reviewable on their own, before any framework-specific code +exists to hide behind. + +Two properties are only testable because `FAILPROOFAI_SDK_STRICT` exists. Without it +you can prove "the customer's call still worked", but never "we swallowed the +right thing" — and an adapter that swallows `BaseException` passes the first +test while silently breaking cancellation in every async app that installs it. +""" + +import dataclasses +import collections +import logging +import sys +import threading +import types +import warnings + +import pytest + +import failproofai_sdk +from failproofai_sdk import integrations +from failproofai_sdk.integrations import _compat, _core + +INTEGRATIONS_LOGGER = "failproofai_sdk.integrations" + + +@pytest.fixture(autouse=True) +def _integration_state(monkeypatch): + """Both strict flags off, no degraded sites, no already-shown warnings. + + All of this is process-global by design (a degraded hook must stay degraded + for the life of the process), so it has to be reset around every test or the + order of the file changes its result. + """ + monkeypatch.delenv("FAILPROOFAI_SDK_STRICT", raising=False) + monkeypatch.delenv("FAILPROOFAI_SDK_STRICT_INTEGRATIONS", raising=False) + _reset_flags() + yield + _reset_flags() + + +def _reset_flags(): + _core.set_strict(None) + _compat.set_strict_integrations(None) + _core.reset_failures() + _compat.reset_warnings() + + +def strict_on(monkeypatch, var="FAILPROOFAI_SDK_STRICT"): + monkeypatch.setenv(var, "1") + _core.set_strict(None) + _compat.set_strict_integrations(None) + + +class FakeAdapter: + """The shape `failproofai_sdk/integrations/.py` has to implement.""" + + def __init__(self, name="fake", module="fakeframework", fail_install=False): + self.name = name + self.module = module + self.fail_install = fail_install + self.installs = 0 + self.uninstalls = 0 + self.options = None + + def install(self, **options): + self.installs += 1 + self.options = options + if self.fail_install: + raise RuntimeError("adapter install exploded") + + def uninstall(self): + self.uninstalls += 1 + + +def register(monkeypatch, adapter, *, name=None, detect=None): + """Put an adapter into the real registry for the duration of one test.""" + name = name or adapter.name + module_path = f"agenteye_fake_{name}" + module = types.ModuleType(module_path) + module.adapter = adapter + monkeypatch.setitem(sys.modules, module_path, module) + monkeypatch.setitem(integrations._REGISTRY, name, module_path) + monkeypatch.setitem(integrations._DETECT, name, detect or (adapter.module,)) + return adapter + + +@pytest.fixture() +def fake(monkeypatch): + adapter = register(monkeypatch, FakeAdapter()) + yield adapter + # Before monkeypatch unwinds the registry, so _ACTIVE cannot keep a + # reference to a test-scoped adapter. + integrations.uninstrument() + + +# --------------------------------------------------------------------------- +# registry +# --------------------------------------------------------------------------- + +def test_the_four_adapters_are_pre_registered(): + # Registered before their modules exist so that adding an adapter is one new + # file, never an edit to the registry (which four agents would conflict on). + assert integrations.available() == ("crewai", "langchain", "llama_index", "pydantic_ai") + + +@pytest.mark.parametrize( + "spelling,expected", + [ + ("langgraph", "langchain"), + ("LangGraph", "langchain"), + ("llamaindex", "llama_index"), + ("llama-index", "llama_index"), + ("pydantic-ai", "pydantic_ai"), + (" crewai ", "crewai"), + ], +) +def test_aliases_resolve(spelling, expected): + assert integrations._canonical(spelling) == expected + + +def test_unknown_name_raises_valueerror_listing_the_valid_ones(): + # A typo that silently records nothing is the worst outcome available. + with pytest.raises(ValueError) as excinfo: + integrations.instrument("langhcain") + message = str(excinfo.value) + assert "langhcain" in message + for name in ("langchain", "crewai", "llama_index", "pydantic_ai"): + assert name in message + + +def test_instrument_installs_and_reports_the_name(fake): + assert integrations.instrument("fake") == ("fake",) + assert fake.installs == 1 + assert integrations.active() == ("fake",) + + +def test_the_public_facade_reaches_the_registry(fake): + # `failproofai_sdk.instrument` imports this package inside the function body, so + # this is also the test that the lazy wiring is connected at all. + assert failproofai_sdk.instrument("fake") == ("fake",) + assert failproofai_sdk.uninstrument("fake") == ("fake",) + with pytest.raises(ValueError): + failproofai_sdk.instrument("bogus") + + +def test_options_reach_the_adapter(fake): + integrations.instrument("fake", session_id="s-1", capture_inputs=False) + assert fake.options == {"session_id": "s-1", "capture_inputs": False} + + +def test_instrumenting_twice_is_a_noop(fake): + assert integrations.instrument("fake") == ("fake",) + assert integrations.instrument("fake") == () + assert fake.installs == 1 + + +def test_uninstrument_calls_uninstall_once(fake): + integrations.instrument("fake") + assert integrations.uninstrument("fake") == ("fake",) + assert fake.uninstalls == 1 + assert integrations.active() == () + + +def test_uninstrument_of_something_never_instrumented_is_a_noop(fake): + assert integrations.uninstrument("fake") == () + assert fake.uninstalls == 0 + + +def test_uninstrument_of_an_unknown_name_does_not_raise(caplog): + # Teardown that can fail is teardown people stop calling. + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + assert integrations.uninstrument("nope") == () + assert "nope" in caplog.text + + +def test_bare_instrument_with_nothing_imported_warns_rather_than_going_quiet( + monkeypatch, caplog +): + # The import-order mistake — calling instrument() above the `import + # langchain` line — instruments nothing and is otherwise indistinguishable + # from working: no exception, no events, adapter "installed". The message + # naming the fix existed at debug level, where no default logging config + # shows it, so the one mistake that costs a user all of their telemetry was + # the one mistake the SDK said nothing about. + # + # Detection is pointed at modules that cannot be imported, rather than the + # registry being emptied: tests/integrations/ runs first and imports all + # four frameworks, so without this `instrument()` would install them for + # real and leak `_ACTIVE` into every test after this one — but an empty + # registry would also render the message's list of valid names as nothing, + # which is the half of it most worth asserting. + monkeypatch.setattr( + integrations, + "_DETECT", + {name: ("failproofai_sdk_no_such_framework",) for name in integrations._REGISTRY}, + ) + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + assert integrations.instrument() == () + assert "NOTHING was instrumented" in caplog.text + assert [r for r in caplog.records if r.levelno >= logging.WARNING] + # It must name what this call WOULD have accepted. Suggesting one hardcoded + # framework leaves a reader who is not using it unsure whether the message + # is a suggestion or a diagnosis. + for name in integrations._REGISTRY: + assert f"instrument({name!r})" in caplog.text + + +def test_uninstrument_all_removes_everything(monkeypatch): + one = register(monkeypatch, FakeAdapter(name="fake1", module="fw1")) + two = register(monkeypatch, FakeAdapter(name="fake2", module="fw2")) + integrations.instrument("fake1") + integrations.instrument("fake2") + assert sorted(integrations.uninstrument()) == ["fake1", "fake2"] + assert one.uninstalls == 1 and two.uninstalls == 1 + assert integrations.active() == () + + +def test_a_failing_install_is_skipped_and_the_others_still_install(monkeypatch, caplog): + broken = register(monkeypatch, FakeAdapter(name="broken", module="fw1", fail_install=True)) + good = register(monkeypatch, FakeAdapter(name="good", module="fw2")) + try: + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + assert integrations.instrument("broken") == () + assert integrations.instrument("good") == ("good",) + assert broken.installs == 1 and good.installs == 1 + assert integrations.active() == ("good",) + assert "broken" in caplog.text + finally: + integrations.uninstrument() + + +def test_a_failing_install_raises_under_strict(monkeypatch): + register(monkeypatch, FakeAdapter(name="broken", module="fw1", fail_install=True)) + strict_on(monkeypatch) + with pytest.raises(RuntimeError, match="exploded"): + integrations.instrument("broken") + assert integrations.active() == () + + +def test_an_adapter_missing_install_is_rejected(monkeypatch, caplog): + register(monkeypatch, types.SimpleNamespace(name="halfbaked", module="fw"), name="halfbaked") + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + assert integrations.instrument("halfbaked") == () + assert "halfbaked" in caplog.text + + +# --------------------------------------------------------------------------- +# auto-detection +# --------------------------------------------------------------------------- + +def test_autodetect_ignores_a_framework_that_is_not_imported(fake): + assert "fakeframework" not in sys.modules + assert "fake" not in integrations.instrument() + + +def test_autodetect_picks_up_a_framework_that_is_imported(fake, monkeypatch): + monkeypatch.setitem(sys.modules, "fakeframework", types.ModuleType("fakeframework")) + assert "fake" in integrations.instrument() + assert fake.installs == 1 + + +def test_autodetect_ignores_an_installed_but_unimported_framework(fake, monkeypatch, tmp_path): + """The test that actually separates `sys.modules` from `find_spec`. + + A framework merely *installed* must not be instrumented — the earlier test + passes against a `find_spec` implementation too, because a stub in + `sys.modules` has no spec to find. Here the module is genuinely importable + and genuinely not imported, which is the common case on any machine with + more than one framework in its virtualenv. + """ + import importlib.util + + (tmp_path / "unimported_framework.py").write_text("raise AssertionError('imported!')\n") + monkeypatch.syspath_prepend(str(tmp_path)) + monkeypatch.setitem(integrations._DETECT, "fake", ("unimported_framework",)) + + assert importlib.util.find_spec("unimported_framework") is not None + assert "unimported_framework" not in sys.modules + + assert "fake" not in integrations.instrument() + assert fake.installs == 0 + assert "unimported_framework" not in sys.modules + + +def test_autodetect_never_imports_the_framework(fake, monkeypatch): + """`sys.modules`, not `find_spec`. + + `find_spec` would say "installed" for a framework the user has not imported, + and acting on that means charging 200ms of transitive imports to a library + that was supposed to be invisible. + """ + imported = [] + real_import_module = integrations.importlib.import_module + + def spy(name, *args, **kwargs): + imported.append(name) + return real_import_module(name, *args, **kwargs) + + monkeypatch.setattr(integrations.importlib, "import_module", spy) + integrations.instrument() + assert "fakeframework" not in imported + + +# --------------------------------------------------------------------------- +# install / uninstall discipline +# --------------------------------------------------------------------------- + +def original_function(x): + return x * 2 + + +def test_uninstall_restores_the_exact_original_object(): + # `is`, not `==`. Restoring by re-importing hands back whatever the current + # value is, which is how two instrumentation libraries un-patch each other. + holder = types.SimpleNamespace(fn=original_function) + patcher = _core.Patcher() + patcher.patch(holder, "fn", _core.wrap_callable(holder.fn, before=lambda *a, **k: None)) + assert holder.fn is not original_function + assert holder.fn(3) == 6 + patcher.restore_all() + assert holder.fn is original_function + + +def test_patching_an_absent_attribute_removes_it_again(): + holder = types.SimpleNamespace() + patcher = _core.Patcher() + patcher.patch(holder, "hook", lambda: None) + assert hasattr(holder, "hook") + patcher.restore_all() + assert not hasattr(holder, "hook") + + +def test_uninstall_declines_when_a_third_party_patched_on_top(caplog): + holder = types.SimpleNamespace(fn=original_function) + patcher = _core.Patcher() + patcher.patch(holder, "fn", _core.wrap_callable(holder.fn, before=lambda *a, **k: None)) + + def third_party(x): + return x + + holder.fn = third_party # somebody else instrumented after us + + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + patcher.restore_all() + + # Restoring here would silently delete their patch. + assert holder.fn is third_party + assert "not restoring" in caplog.text + + +def test_every_wrapper_carries_the_original(): + wrapped = _core.wrap_callable(original_function, before=lambda *a, **k: None) + assert _core.is_wrapped(wrapped) + assert _core.unwrap(wrapped) is original_function + assert wrapped.__name__ == "original_function" + assert _core.unwrap(original_function) is original_function + + +# --------------------------------------------------------------------------- +# failure policy +# --------------------------------------------------------------------------- + +def test_a_raising_hook_leaves_the_call_untouched_and_logs_once(caplog): + def before(*args, **kwargs): + raise RuntimeError("hook boom") + + wrapped = _core.wrap_callable(original_function, before=before) + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + assert wrapped(21) == 42 + warnings_logged = [r for r in caplog.records if r.levelno == logging.WARNING] + assert len(warnings_logged) == 1 + assert warnings_logged[0].exc_info is not None + + +def test_a_raising_hook_reraises_under_strict(monkeypatch): + def before(*args, **kwargs): + raise RuntimeError("hook boom") + + wrapped = _core.wrap_callable(original_function, before=before) + strict_on(monkeypatch) + with pytest.raises(RuntimeError, match="hook boom"): + wrapped(21) + + +def test_a_hot_failing_hook_is_logged_once_then_disabled(caplog): + calls = [] + + def before(*args, **kwargs): + calls.append(1) + raise RuntimeError("boom") + + wrapped = _core.wrap_callable(original_function, before=before) + with caplog.at_level(logging.DEBUG, logger=INTEGRATIONS_LOGGER): + for _ in range(10): + assert wrapped(1) == 2 + + # Stops being called at all: a broken adapter costs one log line, not 40% + # of the process. + assert len(calls) == _core._MAX_FAILURES + assert len([r for r in caplog.records if r.levelno == logging.WARNING]) == 1 + assert len([r for r in caplog.records if r.levelno == logging.ERROR]) == 1 + + +def test_safe_does_not_swallow_baseexception(): + """CancelledError is a BaseException, and eating it breaks cancellation. + + A `safe()` that caught `BaseException` would pass every other test in this + file while silently making every instrumented async application unkillable. + """ + + @_core.safe + def handler(): + raise KeyboardInterrupt("ctrl-c") + + with pytest.raises(KeyboardInterrupt): + handler() + + +def test_safe_swallows_exception_and_returns_none(caplog): + @_core.safe + def handler(): + raise ValueError("nope") + + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + assert handler() is None + assert caplog.records + + +def test_the_structural_guarantee_return_value(): + """before AND after both raising still returns the original's value.""" + sentinel = object() + + def boom(*args, **kwargs): + raise RuntimeError("hook") + + wrapped = _core.wrap_callable(lambda: sentinel, before=boom, after=boom) + assert wrapped() is sentinel + + +def test_the_structural_guarantee_exception_identity(): + """The original exception object comes back out unchanged.""" + original_exc = ValueError("the real failure") + + def raiser(): + raise original_exc + + def boom(*args, **kwargs): + raise RuntimeError("hook") + + wrapped = _core.wrap_callable(raiser, before=boom, after=boom, on_error=boom) + with pytest.raises(ValueError) as excinfo: + wrapped() + # `is`, not a message match: a wrapper that re-raised a copy would break + # `except MyError as e: e.retry_after`. + assert excinfo.value is original_exc + + +def test_on_error_sees_the_exception_and_the_before_context(): + seen = {} + + def before(*args, **kwargs): + return {"args": args} + + def on_error(ctx, exc): + seen["ctx"] = ctx + seen["exc"] = exc + + failure = KeyError("k") + + def raiser(x): + raise failure + + wrapped = _core.wrap_callable(raiser, before=before, on_error=on_error) + with pytest.raises(KeyError): + wrapped(7) + assert seen["ctx"] == {"args": (7,)} + assert seen["exc"] is failure + + +def test_after_sees_the_result(): + seen = {} + wrapped = _core.wrap_callable( + original_function, + before=lambda *a, **k: "ctx", + after=lambda ctx, result: seen.update(ctx=ctx, result=result), + ) + assert wrapped(4) == 8 + assert seen == {"ctx": "ctx", "result": 8} + + +# --------------------------------------------------------------------------- +# payload discipline +# --------------------------------------------------------------------------- + +def test_truncate_marks_and_bounds_a_long_string(): + out = _core.truncate("x" * 20_000, 100) + assert len(out) == 100 + assert out.endswith(_core.TRUNCATION_MARKER) + + +def test_truncate_recurses_into_containers(): + out = _core.truncate({"a": ["y" * 500]}, 50) + assert out["a"][0].endswith(_core.TRUNCATION_MARKER) + + +def test_truncate_leaves_small_values_alone(): + value = {"a": 1, "b": "short", "c": None, "d": True} + assert _core.truncate(value) == value + + +def test_truncate_expands_a_mapping_that_is_not_a_dict(): + # `isinstance(value, dict)` missed every mapping a framework actually hands + # us that is not literally a dict, and those fell through to the repr + # branch. A tool's JSON schema then reached the events store as the STRING + # "mappingproxy({'title': 'From Unit', 'type': 'string'})" — valid JSON + # holding a Python repr, so `JSONExtract` over it finds nothing and the + # field is unqueryable rather than merely ugly. Seen in a real stored row: + # a crewai `model_request.tools[0]…properties.from_unit`. + # `MappingProxyType` is what `model_json_schema()` and any frozen config + # hands back, so this is the common case, not an exotic one. + schema = types.MappingProxyType({"title": "From Unit", "type": "string"}) + assert _core.truncate({"from_unit": schema}) == { + "from_unit": {"title": "From Unit", "type": "string"} + } + assert _core.truncate({"p": collections.ChainMap({"a": 1})}) == {"p": {"a": 1}} + + +def test_truncate_expands_a_dataclass_and_a_pydantic_model(): + # Every framework hands us these — a tool's argument model, its structured + # return, a settings object on a request — and they have no JSON shape by + # the container checks, so they were rendered: `Point(x=1, y=2)`, a Python + # repr inside a JSON string that `JSONExtract` cannot read and the dashboard + # cannot filter on. + @dataclasses.dataclass + class Point: + x: int + y: int + + class Model: # duck-typed pydantic v2: what matters is `model_dump` + def model_dump(self): + return {"city": "Faro", "celsius": 21} + + assert _core.truncate({"p": Point(1, 2)}) == {"p": {"x": 1, "y": 2}} + assert _core.truncate({"m": Model()}) == {"m": {"city": "Faro", "celsius": 21}} + # and nested inside a container, which is where they actually arrive + assert _core.truncate([Point(1, 2)]) == [{"x": 1, "y": 2}] + + +def test_truncate_falls_back_to_repr_when_unwrapping_raises(): + # `model_dump` and `getattr` both run the CALLER'S code — a validator, a + # property that touches the network. A telemetry library must not turn that + # into an exception in someone's agent loop, and `repr` is exactly what + # would have happened before any of this existed. + class Exploding: + def model_dump(self): + raise RuntimeError("boom") + + def __repr__(self): + return "" + + assert _core.truncate({"e": Exploding()}) == {"e": ""} + + +def test_truncate_does_not_mistake_a_class_for_an_instance(): + # `dataclasses.is_dataclass` is true of the CLASS as well as its instances, + # and `fields()` on the class would render a type as though it were data. + @dataclasses.dataclass + class Point: + x: int + + out = _core.truncate({"c": Point}) + assert isinstance(out["c"], str) and "class" in out["c"] + + +def test_truncate_still_reprs_an_object_with_no_json_shape(): + # The counterweight: widening to the ABCs must not turn the repr branch + # into dead code. An object that is neither a mapping nor a sequence has no + # JSON shape and rendering it is the only thing left to do. + class Opaque: + def __repr__(self): + return "" + + assert _core.truncate({"o": Opaque()}) == {"o": ""} + # And a str is a Sequence — it must keep taking the string path, not be + # exploded into a list of characters. + assert _core.truncate("hello") == "hello" + assert _core.truncate(b"raw") == "raw" + + +def test_payload_flags_truncation(): + out = _core.payload({"fw_prompt": "z" * 100_000}) + assert out["fw_truncated"] is True + assert len(out["fw_prompt"]) <= _core.FIELD_LIMIT + + +def test_payload_enforces_the_event_budget(): + fields = {f"fw_{i}": "q" * _core.FIELD_LIMIT for i in range(20)} + out = _core.payload(fields) + assert out["fw_truncated"] is True + total = sum(len(v) for v in out.values() if isinstance(v, str)) + assert total <= _core.EVENT_BUDGET + + +def test_payload_leaves_a_small_event_alone(): + assert _core.payload({"fw_node": "retrieve"}) == {"fw_node": "retrieve"} + + +def test_fw_fields_namespaces_and_drops_none(): + assert _core.fw_fields(run_id="r1", node="retrieve", tags=None) == { + "fw_run_id": "r1", + "fw_node": "retrieve", + } + + +def test_fw_fields_namespaces_a_name_that_would_shadow_a_column(): + # `_schema._build()` merges extras last, so a bare `tool_name` extra would + # overwrite the declared one and change the promoted column. + assert _core.fw_fields(tool_name="sneaky") == {"fw_tool_name": "sneaky"} + + +def test_fw_fields_passes_the_deliberate_top_level_names_through(): + out = _core.fw_fields(duration_ms=12, request_id="req-1", usage={"total_tokens": 3}) + assert out == {"duration_ms": 12, "request_id": "req-1", "usage": {"total_tokens": 3}} + + +def test_forbidden_extras_is_derived_from_the_schema(): + # Derived, not hand-listed, so adding a field to _schema.py cannot leave a + # stale copy here. + for name in ("tool_name", "model", "outcome", "input_tokens", "error", "session_id"): + assert name in _core.FORBIDDEN_EXTRAS + for name in ("request_id", "duration_ms", "usage", "framework"): + assert name not in _core.FORBIDDEN_EXTRAS + + +def test_guard_extras_strips_a_shadowing_name(caplog): + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + out = _core.guard_extras({"tool_name": "sneaky", "fw_node": "ok"}) + assert out == {"fw_node": "ok"} + assert "tool_name" in caplog.text + + +def test_guard_extras_raises_under_strict(monkeypatch): + strict_on(monkeypatch) + with pytest.raises(ValueError, match="tool_name"): + _core.guard_extras({"tool_name": "sneaky"}) + + +@pytest.mark.parametrize( + "raw,expected", + [ + (" retrieve\n documents ", "retrieve documents"), + ("550e8400-e29b-41d4-a716-446655440000", "main"), + ("550e8400e29b41d4a716446655440000", "main"), + ("deadbeefdeadbeefdead", "main"), + ("", "main"), + (" ", "main"), + (None, "main"), + ("Researcher", "Researcher"), + ("node_" * 40, ("node_" * 40)[:64]), + # A readable name carrying a per-run id. `_looks_like_id` fires only on + # a value that is an id all the way through, so these went in untouched + # — one distinct value per run, into the LowCardinality column that is + # the primary dashboard facet. That is the same poisoning the + # whole-string guard exists to prevent, reached by the shape frameworks + # actually produce and the docs already warn about. + ("agent-550e8400e29b41d4a716446655440000", "agent"), + ("task-550e8400-e29b-41d4-a716-446655440000", "task"), + ("crew_550e8400-e29b-41d4-a716-446655440000_worker", "crew worker"), + # ...and the counterweight: stripping must not eat a readable name that + # merely contains short hex-ish or numeric segments. + ("agent-v2", "agent-v2"), + ("step-3", "step-3"), + ("node_a1b2", "node_a1b2"), + ("deadbeef", "deadbeef"), + ], +) +def test_normalize_agent_id(raw, expected): + # agent_id is LowCardinality(String) and the primary dashboard facet; a UUID + # in it poisons that facet for every session in the project. + assert _core.normalize_agent_id(raw, "main") == expected + + +def test_ms_is_always_an_int(): + # The server stores duration_ms as u32 and its JSON parser drops floats, + # which silently NULLs the column. + from datetime import timedelta + + assert _core.ms(1.2345) == 1234 + assert isinstance(_core.ms(0.5), int) + assert _core.ms(timedelta(milliseconds=250)) == 250 + assert _core.ms(-3.0) == 0 + + +def test_framework_fields(): + out = _core.framework_fields("langchain", "definitely-not-installed") + assert out["framework"] == "langchain" + assert out["integration_version"] == failproofai_sdk.__version__ + assert "framework_version" not in out + + +# --------------------------------------------------------------------------- +# RunTracker +# --------------------------------------------------------------------------- + +def test_run_tracker_brackets_an_agent(events): + tracker = _core.RunTracker("fake") + tracker.start_agent("run-1", agent_id="researcher", session_id="s-1", goal="find it") + tracker.end_agent("run-1", outcome="success") + assert events.types() == ["agent_start", "agent_end"] + start, end = events.entries + assert start["session_id"] == "s-1" + assert start["agent_id"] == "researcher" + assert start["goal"] == "find it" + assert "parent_id" not in start + assert end["outcome"] == "success" + + +def test_run_tracker_uses_a_uuid_run_id_as_a_key_not_as_an_agent_id(events): + tracker = _core.RunTracker("fake") + tracker.start_agent( + "run-1", agent_id="550e8400-e29b-41d4-a716-446655440000", session_id="s-1" + ) + assert events.last()["agent_id"] == "main" + + +def test_run_tracker_nests_on_the_parent_key(events): + tracker = _core.RunTracker("fake") + tracker.start_agent("root", agent_id="crew", session_id="s-1") + tracker.start_agent("child", agent_id="researcher", parent_key="root") + child = events.entries[-1] + assert child["parent_id"] == "crew" + assert child["session_id"] == "s-1" + + +def test_run_tracker_walks_a_chain_of_non_agent_runs(events): + """The framework's parent_run_id chain is a better parent than a contextvar + stack: it survives task hops and thread pools.""" + tracker = _core.RunTracker("fake") + tracker.start_agent("agent-run", agent_id="graph", session_id="s-1") + tracker.link("chain-run", "agent-run") + tracker.link("inner-run", "chain-run") + tracker.emit( + "tool_use", "tool-run", parent_key="inner-run", tool_name="search", tool_call_id="t1" + ) + tool = events.entries[-1] + assert tool["type"] == "tool_use" + assert tool["agent_id"] == "graph" + assert tool["session_id"] == "s-1" + + +def test_run_tracker_joins_a_hand_written_scope(events): + """Step 3 of `identity()` — the whole interop story. + + An adapter running inside a hand-written `with failproofai_sdk.agent("planner")` + must land in the SAME session with parent_id="planner", or the customer gets + two disconnected trees for one run. + """ + tracker = _core.RunTracker("fake") + with failproofai_sdk.agent("planner", session_id="s-outer", goal="q"): + tracker.start_agent("run-1", agent_id="researcher") + tracker.emit("tool_use", "run-1", tool_name="search", tool_call_id="t1") + tracker.end_agent("run-1") + # A run the tracker has never seen — a callback that arrived without a + # start, which every framework produces eventually — still lands on the + # open scope rather than being dropped or given a session of its own. + tracker.emit("tool_use", "never-registered", tool_name="lookup", tool_call_id="t2") + + assert events.types() == [ + "agent_start", + "agent_start", + "tool_use", + "agent_end", + "tool_use", + "agent_end", + ] + assert all(e["session_id"] == "s-outer" for e in events.entries) + researcher = events.entries[1] + assert researcher["agent_id"] == "researcher" + assert researcher["parent_id"] == "planner" + assert events.entries[2]["agent_id"] == "researcher" + assert events.entries[4]["agent_id"] == "planner" + + +def test_run_tracker_does_not_invent_a_parent_inside_a_bare_session(events): + """A `session()` scope binds no agent, so an adapter agent there is a root. + + Claiming `parent_id="main"` would point at an agent that never emitted an + `agent_start`, and the dashboard answers that by synthesizing a permanent + extra lane that is `ongoing` forever. + """ + tracker = _core.RunTracker("fake") + with failproofai_sdk.session("s-outer"): + tracker.start_agent("run-1", agent_id="researcher") + start = events.entries[0] + assert start["session_id"] == "s-outer" + assert "parent_id" not in start + + +def test_run_tracker_drops_unresolvable_events_and_warns_once(events, caplog): + tracker = _core.RunTracker("fake") + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + for i in range(5): + tracker.emit("tool_use", f"unknown-{i}", tool_name="t", tool_call_id="x") + assert events.entries == [] + # Dropped rather than given a synthesized session id: a synthetic session + # splits one run into many, which is a silent wrong answer. + resolution_warnings = [r for r in caplog.records if "could not resolve" in r.getMessage()] + assert len(resolution_warnings) == 1 + + +def test_run_tracker_is_bounded(events): + tracker = _core.RunTracker("fake", max_open=10) + for i in range(200): + tracker.start_agent(f"run-{i}", agent_id="worker", session_id="s-1") + assert len(tracker.open_agents()) <= 10 + + +def test_run_tracker_survives_concurrent_start_and_end(events): + """CrewAI dispatches its handlers on a ten-worker pool.""" + tracker = _core.RunTracker("fake") + errors = [] + barrier = threading.Barrier(8) + + def worker(n): + try: + barrier.wait(timeout=10) + for i in range(50): + key = f"{n}-{i}" + tracker.start_agent(key, agent_id="worker", session_id="s-1") + tracker.emit("tool_use", key, tool_name="t", tool_call_id=key) + tracker.end_agent(key) + except BaseException as exc: # noqa: BLE001 - reported, not swallowed + errors.append(exc) + + threads = [threading.Thread(target=worker, args=(n,)) for n in range(8)] + for thread in threads: + thread.start() + for thread in threads: + thread.join(timeout=30) + + assert errors == [] + assert tracker.open_agents() == () + assert len(events.entries) == 8 * 50 * 3 + + +def test_run_tracker_close_open_agents(events): + tracker = _core.RunTracker("fake") + tracker.start_agent("a", agent_id="one", session_id="s-1") + tracker.start_agent("b", agent_id="two", parent_key="a") + tracker.close_open_agents() + # A session that dies with an open agent_start renders `ongoing` forever. + assert events.types() == ["agent_start", "agent_start", "agent_end", "agent_end"] + assert [e["agent_id"] for e in events.entries[2:]] == ["two", "one"] + assert all(e["outcome"] == "cancelled" for e in events.entries[2:]) + + +def test_run_tracker_stamps_the_framework_fields(events): + tracker = _core.RunTracker("fake", base_fields=_core.framework_fields("fake")) + tracker.start_agent("run-1", agent_id="worker", session_id="s-1") + assert events.last()["framework"] == "fake" + assert events.last()["integration_version"] == failproofai_sdk.__version__ + + +def test_run_tracker_base_fields_cannot_shadow_a_declared_field(events, caplog): + tracker = _core.RunTracker("fake", base_fields={"tool_name": "hijacked"}) + tracker.start_agent("run-1", agent_id="worker", session_id="s-1") + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + tracker.emit("tool_use", "run-1", tool_name="real", tool_call_id="t1") + assert events.last()["tool_name"] == "real" + + +def test_run_tracker_truncates_a_huge_payload(events): + tracker = _core.RunTracker("fake") + tracker.start_agent("run-1", agent_id="worker", session_id="s-1") + tracker.emit( + "tool_use", "run-1", tool_name="t", tool_call_id="t1", input={"q": "x" * 100_000} + ) + assert len(events.last()["input"]["q"]) <= _core.FIELD_LIMIT + + +def test_run_tracker_never_raises_into_the_caller(events, caplog): + tracker = _core.RunTracker("fake") + tracker.start_agent("run-1", agent_id="worker", session_id="s-1") + with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER): + # `timestamp` is reserved, so the emit raises inside the SDK. + tracker.emit("tool_use", "run-1", tool_name="t", tool_call_id="t1", timestamp="nope") + assert "failproofai_sdk" in caplog.text + + +def test_run_tracker_raises_into_the_caller_under_strict(events, monkeypatch): + tracker = _core.RunTracker("fake") + tracker.start_agent("run-1", agent_id="worker", session_id="s-1") + strict_on(monkeypatch) + with pytest.raises(ValueError): + tracker.emit("tool_use", "run-1", tool_name="t", tool_call_id="t1", timestamp="nope") + + +def test_run_tracker_does_not_touch_contextvars(events): + """Shape B must never bind identity onto the context. + + `ContextVar.reset(token)` raises across asyncio tasks as well as threads, so + a token cannot be held between two callbacks — and a scope entered in + `on_tool_start` that is never exited misattributes every later event in the + process. + """ + from failproofai_sdk import _context + + tracker = _core.RunTracker("fake") + tracker.start_agent("run-1", agent_id="worker", session_id="s-1") + assert _context.snapshot() == (None, ()) + tracker.emit("tool_use", "run-1", tool_name="t", tool_call_id="t1") + assert _context.snapshot() == (None, ()) + tracker.end_agent("run-1") + assert _context.snapshot() == (None, ()) + + +# --------------------------------------------------------------------------- +# _compat +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize( + "text,expected", + [ + ("1.5.2", (1, 5, 2)), + ("2.0.0b1", (2, 0, 0)), + ("0.14.23.post1", (0, 14, 23)), + ("1.2.dev0", (1, 2)), + ("1", (1,)), + ("", ()), + ("nonsense", ()), + ], +) +def test_parse_version_is_naive_on_purpose(text, expected): + # No `packaging`: `import failproofai_sdk` is zero-dependency. + assert _compat.parse_version(text) == expected + + +def test_version_comparison_orders_as_expected(): + assert _compat.parse_version("1.5") >= _compat.parse_version("1.4.7") + assert _compat.parse_version("1.5.2") < _compat.parse_version("2") + + +def test_missing_framework_raises_with_the_install_command(): + # Tier 1. Instrumenting is an explicit user action, so silence is never right. + with pytest.raises(ImportError) as excinfo: + _compat.require_module("definitely_not_a_module", dist="nope", extra="langchain") + assert "pip install 'failproofai_sdk[langchain]'" in str(excinfo.value) + + +def test_require_module_returns_the_module(): + assert _compat.require_module("json", dist="json", extra="langchain").__name__ == "json" + + +def test_version_in_range_is_silent(recwarn): + assert _compat.check_version("self", "failproofai_sdk", minimum="0.0.1", below="99") is True + assert not [w for w in recwarn if issubclass(w.category, _compat.FailproofAICompatWarning)] + + +def test_version_out_of_range_warns_once(): + # Tier 2: warn, then best effort. A ceiling is what stops a future major + # from making the adapter stop recording while raising nothing. + with pytest.warns(_compat.FailproofAICompatWarning, match="newer"): + assert _compat.check_version("self", "failproofai_sdk", below="0.0.1") is False + + # Deduplicated: these fire from install() *and* from hot callbacks, so a + # per-call warning on a chatty framework is its own outage. + with warnings.catch_warnings(record=True) as second: + warnings.simplefilter("always") + _compat.check_version("self", "failproofai_sdk", below="0.0.1") + assert not [w for w in second if issubclass(w.category, _compat.FailproofAICompatWarning)] + + +def test_an_uninstallable_distribution_is_best_effort(): + assert _compat.check_version("x", "definitely-not-installed", minimum="99") is True + + +def test_strict_integrations_promotes_a_warning_to_an_exception(monkeypatch): + strict_on(monkeypatch, "FAILPROOFAI_SDK_STRICT_INTEGRATIONS") + with pytest.raises(_compat.FailproofAICompatWarning): + _compat.check_version("self", "failproofai_sdk", minimum="99") + + +def test_a_failing_capability_probe_disables_one_hook_only(): + # Tier 3. + with pytest.warns(_compat.FailproofAICompatWarning, match="on_interrupt"): + assert _compat.probe("langchain", "on_interrupt", lambda: 1 / 0) is False + assert _compat.probe("langchain", "on_tool_start", lambda: True) is True + + +def test_a_probe_returning_false_warns(): + with pytest.warns(_compat.FailproofAICompatWarning, match="does not provide"): + assert _compat.probe("langchain", "on_resume", lambda: None) is False diff --git a/sdk/python/tests/test_packaging.py b/sdk/python/tests/test_packaging.py new file mode 100644 index 000000000..d8969e566 --- /dev/null +++ b/sdk/python/tests/test_packaging.py @@ -0,0 +1,169 @@ +"""Packaging contracts, read from `importlib.metadata` — never from pyproject. + +Two reasons this file never opens `pyproject.toml`: + +1. `tomllib` is 3.11+ and this SDK's floor is 3.10, so parsing it here would + make the packaging tests skip exactly on the leg that catches 3.10 problems. +2. `pyproject.toml` is the *input*. What ships is the wheel's `METADATA`, and + the failure mode these tests exist to catch — an extra that silently drags + the wrong `failproofai_sdk` off PyPI — happens at install time, from METADATA. + +The rules being enforced: + +* **zero runtime dependencies.** `import failproofai_sdk` must work in an environment + with nothing else installed, so every `Requires-Dist` carries an + `extra == "..."` marker. +* **no self-referential extra.** On public PyPI `failproofai_sdk` is the *CLI*; a + `Requires-Dist: failproofai_sdk[...]` would install the CLI over this SDK. This is + also why the adapters ship as extras on one distribution rather than as + `failproofai_sdk-langgraph` and friends. +* **no `[all]` extra** — a resolution bomb that exists only for CI's + convenience. +* **the extras and the adapter registry agree.** They are two hand-maintained + lists of the same set, in two files, with no codegen between them. +""" + +import re +import os +import subprocess +from pathlib import Path + +import failproofai_sdk +import sys +import textwrap + +import importlib.metadata as metadata + +import pytest + +from failproofai_sdk import integrations + +DIST = "failproofai_sdk" + + +def requirements() -> list[str]: + return list(metadata.requires(DIST) or []) + + +def declared_extras() -> set[str]: + return set(metadata.distribution(DIST).metadata.get_all("Provides-Extra") or []) + + +def test_the_distribution_is_installed(): + # Guards every other test in this file: `requires()` returns None for a + # dist that is not installed, and `for req in []` passes vacuously. + assert metadata.version(DIST) + assert requirements() + + +def test_every_requirement_is_gated_behind_an_extra(): + ungated = [req for req in requirements() if "extra ==" not in req] + assert ungated == [], ( + "failproofai_sdk must have ZERO runtime dependencies; these would be installed " + f"unconditionally: {ungated}" + ) + + +def test_no_requirement_names_the_retired_distribution(): + for req in requirements(): + name = re.split(r"[\s\[<>=!~;(]", req.strip(), maxsplit=1)[0] + assert name.lower().replace("_", "-") != DIST, ( + f"self-referential requirement {req!r}: on public PyPI `failproofai_sdk` is the " + "CLI, so this would install the CLI over the SDK" + ) + + +def test_there_is_no_all_extra(): + assert "all" not in declared_extras() + + +def test_every_extra_maps_to_a_registered_adapter(): + for extra in declared_extras() - {"dev"}: + # Raises ValueError if the extra is not a name (or alias) the registry + # knows — i.e. an extra nobody can instrument. + assert integrations._canonical(extra) in integrations.available() + + +def test_every_registered_adapter_has_an_extra(): + covered = {integrations._canonical(extra) for extra in declared_extras() - {"dev"}} + assert covered == set(integrations.available()) + + +def test_every_extra_requirement_pins_a_floor_and_a_ceiling(): + # Without a ceiling, a clean build a year from now pulls the next major, the + # callback API shifts, and the adapter stops receiving events while raising + # nothing at all. + for req in requirements(): + if 'extra == "dev"' in req: + continue + assert ">=" in req, f"{req} has no floor" + assert "<" in req, f"{req} has no ceiling" + + +IMPORT_GUARD = textwrap.dedent( + """ + import sys + + baseline = set(sys.modules) + import failproofai_sdk + + added = set(sys.modules) - baseline + third_party = sorted( + name for name in added + if name.split(".")[0] not in sys.stdlib_module_names + and not name.startswith("failproofai_sdk") + ) + integrations = sorted(n for n in sys.modules if n.startswith("failproofai_sdk.integrations")) + + assert not third_party, "import failproofai_sdk pulled in third-party modules: %r" % third_party + assert not integrations, "import failproofai_sdk imported the adapters: %r" % integrations + assert failproofai_sdk._writer is not sys.modules["failproofai_sdk._writer"], "_writer is the module" + print("OK") + """ +) + + +def test_importing_the_package_stays_clean(tmp_path): + """A subprocess, because the assertion is about a *cold* interpreter. + + In-process, every framework any other test file imported is already in + `sys.modules` and the check is vacuous. + """ + # `cwd=tmp_path` is what makes the interpreter cold — away from the source + # tree, so nothing is importable by accident. PYTHONPATH then puts the + # package back deliberately, which is what lets this run in an environment + # where it is not pip-installed (a container running the suite off a mounted + # checkout). Without it the test does not fail loudly, it fails with + # ModuleNotFoundError and looks like a packaging bug that is not there. + env = dict(os.environ, PYTHONPATH=str(Path(failproofai_sdk.__file__).resolve().parents[1])) + result = subprocess.run( + [sys.executable, "-c", IMPORT_GUARD], + capture_output=True, + text=True, + cwd=str(tmp_path), + env=env, + ) + assert result.returncode == 0, result.stderr + assert "OK" in result.stdout + + +def test_instrument_is_lazy_until_it_is_called(): + # `failproofai_sdk.instrument` is a thin wrapper whose import sits *inside* the + # function body; the guard test above proves the module-level import is + # absent, this proves the wrapper still exists to be called. + assert callable(sdk_instrument()) + + +def sdk_instrument(): + import failproofai_sdk + + return failproofai_sdk.instrument + + +@pytest.mark.parametrize("name", ["langchain", "crewai", "llama_index", "pydantic_ai"]) +def test_the_registry_points_at_a_module_inside_this_package(name): + path = integrations._REGISTRY[name] + assert path.startswith("failproofai_sdk.integrations.") + # The registry entry is a STRING, imported on demand. If this ever becomes a + # module object, `import failproofai_sdk` stops being zero-dependency. + assert isinstance(path, str) diff --git a/sdk/python/tests/test_scopes.py b/sdk/python/tests/test_scopes.py new file mode 100644 index 000000000..49c05a58b --- /dev/null +++ b/sdk/python/tests/test_scopes.py @@ -0,0 +1,487 @@ +"""Tests for session() / agent() / tool_call(). + +The exception table in `_scopes.agent` is the contract most likely to be broken +by a well-meaning refactor, so every row of it has a test *and* every test +asserts event ORDER, not just presence: the dashboard closes the agent span at +`agent_end`, so an `error` emitted after it is attributed to nothing. +""" + +import asyncio +from datetime import datetime, timezone + +import pytest + +import failproofai_sdk +import failproofai_sdk._context as _context +import failproofai_sdk._runtime as _runtime + + +def indexes(entries, event_type): + return [i for i, e in enumerate(entries) if e["type"] == event_type] + + +def only(entries, event_type): + matches = [e for e in entries if e["type"] == event_type] + assert len(matches) == 1, f"expected one {event_type}, got {len(matches)}" + return matches[0] + + +# --------------------------------------------------------------------------- +# session() +# --------------------------------------------------------------------------- + +def test_session_emits_no_events(events): + with failproofai_sdk.session("s-1"): + pass + assert events.entries == [] + + +def test_session_binds_agent_id_without_emitting(events): + with failproofai_sdk.session("s-1", agent_id="worker"): + _runtime.event.agent_start() + entry = only(events.entries, "agent_start") + assert entry["session_id"] == "s-1" + assert entry["agent_id"] == "worker" + + +# --------------------------------------------------------------------------- +# agent() — happy path +# --------------------------------------------------------------------------- + +def test_agent_brackets_the_run(events): + with failproofai_sdk.agent("planner", session_id="s-1", goal="find it"): + pass + assert events.types() == ["agent_start", "agent_end"] + start, end = events.entries + assert start["agent_id"] == "planner" + assert start["goal"] == "find it" + assert "parent_id" not in start + assert end["outcome"] == "success" + + +def test_agent_generates_a_session_id(events): + with failproofai_sdk.agent("planner") as ident: + assert len(ident.session_id) == 32 + assert events.entries[0]["session_id"] == ident.session_id + + +def test_agent_custom_outcome_and_summary(events): + with failproofai_sdk.agent("a", session_id="s", outcome="partial", summary="did half"): + pass + end = only(events.entries, "agent_end") + assert end["outcome"] == "partial" + assert end["summary"] == "did half" + + +def test_agent_extra_fields_land_on_agent_start(events): + with failproofai_sdk.agent("a", session_id="s", framework="langgraph", fw_node="retrieve"): + pass + start = only(events.entries, "agent_start") + assert start["framework"] == "langgraph" + assert start["fw_node"] == "retrieve" + + +def test_agent_default_id_is_main(events): + with failproofai_sdk.agent(session_id="s"): + pass + assert events.entries[0]["agent_id"] == "main" + + +# --------------------------------------------------------------------------- +# Nesting and parent_id +# --------------------------------------------------------------------------- + +def test_parent_ids_follow_the_stack(events): + with failproofai_sdk.agent("a", session_id="s"): + with failproofai_sdk.agent("b"): + with failproofai_sdk.agent("c"): + assert failproofai_sdk.current().depth == 3 + assert failproofai_sdk.current().parent_id == "b" + + starts = [e for e in events.entries if e["type"] == "agent_start"] + assert [e["agent_id"] for e in starts] == ["a", "b", "c"] + assert "parent_id" not in starts[0] + assert starts[1]["parent_id"] == "a" + assert starts[2]["parent_id"] == "b" + + +def test_explicit_parent_none_forces_a_root_span(events): + with failproofai_sdk.agent("a", session_id="s"): + with failproofai_sdk.agent("b", parent_id=None): + pass + start_b = [e for e in events.entries if e["type"] == "agent_start"][1] + assert "parent_id" not in start_b + + +def test_explicit_parent_string_overrides(events): + with failproofai_sdk.agent("a", session_id="s"): + with failproofai_sdk.agent("b", parent_id="somewhere-else"): + pass + start_b = [e for e in events.entries if e["type"] == "agent_start"][1] + assert start_b["parent_id"] == "somewhere-else" + + +def test_nested_agent_inherits_the_session(events): + with failproofai_sdk.agent("a", session_id="s-outer"): + with failproofai_sdk.agent("b"): + pass + assert {e["session_id"] for e in events.entries} == {"s-outer"} + + +def test_stack_unwinds_after_an_exception_through_three_scopes(events): + class Boom(Exception): + pass + + try: + with failproofai_sdk.agent("a", session_id="s"): + with failproofai_sdk.agent("b"): + with failproofai_sdk.agent("c"): + raise Boom("deep") + except Boom as exc: + assert str(exc) == "deep" + else: + pytest.fail("Boom was not raised") + + assert failproofai_sdk.current().depth == 0 + assert _context.snapshot() == (None, ()) + # Each level reports the failure on its own span, innermost first. + assert events.types() == [ + "agent_start", "agent_start", "agent_start", + "error", "agent_end", + "error", "agent_end", + "error", "agent_end", + ] + ends = [e for e in events.entries if e["type"] == "agent_end"] + assert [e["agent_id"] for e in ends] == ["c", "b", "a"] + + +def test_stack_unwinds_even_when_emission_fails(events, monkeypatch): + real_agent_end = _runtime.event.agent_end + + def explode(**kwargs): + if kwargs.get("agent_id") == "b": + raise RuntimeError("writer is down") + return real_agent_end(**kwargs) + + monkeypatch.setattr(_runtime.event, "agent_end", explode) + + with failproofai_sdk.agent("a", session_id="s"): + with pytest.raises(RuntimeError, match="writer is down"): + with failproofai_sdk.agent("b"): + pass + assert failproofai_sdk.current().agent_id == "a" + assert failproofai_sdk.current().depth == 1 + + +def test_failed_agent_start_does_not_leave_a_frame(events): + with pytest.raises(ValueError, match="Reserved field"): + with failproofai_sdk.agent("a", session_id="s", timestamp="nope"): + pass + assert _context.snapshot() == (None, ()) + + +# --------------------------------------------------------------------------- +# The exception table — one test per row, asserting order +# --------------------------------------------------------------------------- + +def test_no_exception_emits_only_agent_end(events): + with failproofai_sdk.agent("a", session_id="s"): + pass + assert events.types() == ["agent_start", "agent_end"] + assert only(events.entries, "agent_end")["outcome"] == "success" + + +def test_exception_emits_error_before_agent_end(events): + try: + with failproofai_sdk.agent("a", session_id="s"): + raise ValueError("nope") + except ValueError as exc: + assert str(exc) == "nope" + else: + pytest.fail("ValueError was not raised") + + types = events.types() + assert types.index("error") < types.index("agent_end") + err = only(events.entries, "error") + assert err["error_type"] == "ValueError" + assert err["message"] == "nope" + assert "ValueError: nope" in err["traceback"] + end = only(events.entries, "agent_end") + # "failed", never "failure": only error|failed|timeout|rejected count as a + # failure server-side. + assert end["outcome"] == "failed" + + +def _assert_failed_base_exception(events, expected_type): + types = events.types() + assert types.index("error") < types.index("agent_end") + assert only(events.entries, "agent_end")["outcome"] == "failed" + assert only(events.entries, "error")["error_type"] == expected_type.__name__ + + +def test_keyboard_interrupt_is_reported_as_failed(events): + try: + with failproofai_sdk.agent("a", session_id="s"): + raise KeyboardInterrupt() + except KeyboardInterrupt: + pass + else: + pytest.fail("KeyboardInterrupt was not raised") + _assert_failed_base_exception(events, KeyboardInterrupt) + + +def test_system_exit_is_reported_as_failed(events): + try: + with failproofai_sdk.agent("a", session_id="s"): + raise SystemExit() + except SystemExit: + pass + else: + pytest.fail("SystemExit was not raised") + _assert_failed_base_exception(events, SystemExit) + + +def _assert_cancelled_without_error(events): + assert events.types() == ["agent_start", "agent_end"] + assert only(events.entries, "agent_end")["outcome"] == "cancelled" + + +def test_cancelled_error_is_not_an_error(events): + try: + with failproofai_sdk.agent("a", session_id="s"): + raise asyncio.CancelledError() + except asyncio.CancelledError: + pass + else: + pytest.fail("CancelledError was not raised") + _assert_cancelled_without_error(events) + + +def test_generator_exit_is_not_an_error(events): + try: + with failproofai_sdk.agent("a", session_id="s"): + raise GeneratorExit() + except GeneratorExit: + pass + else: + pytest.fail("GeneratorExit was not raised") + _assert_cancelled_without_error(events) + + +def test_real_task_cancellation_is_reported_as_cancelled(events): + async def main(): + started = asyncio.Event() + + async def body(): + async with failproofai_sdk.agent("a", session_id="s"): + started.set() + await asyncio.sleep(60) + + task = asyncio.create_task(body()) + await started.wait() + task.cancel() + with pytest.raises(asyncio.CancelledError): + cancelled_result = await task + pytest.fail(f"cancelled task returned {cancelled_result!r}") + + asyncio.run(main()) + assert events.types() == ["agent_start", "agent_end"] + assert only(events.entries, "agent_end")["outcome"] == "cancelled" + + +# --------------------------------------------------------------------------- +# tool_call() +# --------------------------------------------------------------------------- + +def test_tool_call_pairs_and_carries_output(events): + with failproofai_sdk.agent("a", session_id="s"): + with failproofai_sdk.tool_call("web_search", input={"q": "x"}) as t: + t.output = {"hits": 3} + call_id = t.id + + assert events.types() == ["agent_start", "tool_use", "tool_result", "agent_end"] + use = only(events.entries, "tool_use") + res = only(events.entries, "tool_result") + assert use["tool_call_id"] == res["tool_call_id"] == call_id + assert use["input"] == {"q": "x"} + assert res["output"] == {"hits": 3} + assert "error" not in res + assert isinstance(res["duration_ms"], int) + + +def test_tool_call_id_is_read_only(): + box = failproofai_sdk._scopes.ToolCall("abc") + assert box.id == "abc" + with pytest.raises(AttributeError): + box.id = "other" + # __slots__: no stray attributes + with pytest.raises(AttributeError): + box.whatever = 1 + + +def test_tool_call_accepts_an_explicit_id(events): + with failproofai_sdk.agent("a", session_id="s"): + with failproofai_sdk.tool_call("t", tool_call_id="run-42") as t: + assert t.id == "run-42" + assert only(events.entries, "tool_use")["tool_call_id"] == "run-42" + + +def test_tool_call_failure_emits_tool_result_error_and_no_error_event(events): + with failproofai_sdk.agent("a", session_id="s"): + try: + with failproofai_sdk.tool_call("t"): + raise TypeError("bad arg") + except TypeError as exc: + assert str(exc) == "bad arg" + else: + pytest.fail("TypeError was not raised") + + # No `error` event from the tool: an exception the agent loop catches is + # not a run-level error. + assert events.types() == ["agent_start", "tool_use", "tool_result", "agent_end"] + assert only(events.entries, "tool_result")["error"] == "TypeError: bad arg" + assert only(events.entries, "agent_end")["outcome"] == "success" + + +def test_propagating_tool_failure_is_reported_exactly_once(events): + try: + with failproofai_sdk.agent("a", session_id="s"): + with failproofai_sdk.tool_call("t"): + raise TypeError("bad arg") + except TypeError as exc: + assert str(exc) == "bad arg" + else: + pytest.fail("TypeError was not raised") + + assert events.types() == [ + "agent_start", "tool_use", "tool_result", "error", "agent_end", + ] + assert len(indexes(events.entries, "error")) == 1 + types = events.types() + assert types.index("error") < types.index("agent_end") + + +def test_tool_call_uses_the_enclosing_agent_id(events): + with failproofai_sdk.agent("outer", session_id="s"): + with failproofai_sdk.agent("inner"): + with failproofai_sdk.tool_call("t"): + pass + use = only(events.entries, "tool_use") + res = only(events.entries, "tool_result") + assert use["agent_id"] == res["agent_id"] == "inner" + + +def test_tool_call_without_a_session_raises_naming_the_fix(events): + with pytest.raises(TypeError, match="propagate"): + with failproofai_sdk.tool_call("t"): + pass + + +def test_awkward_payload_does_not_break_the_scope(events): + """A non-JSON-serialisable output must not stop `agent_end` landing. + + Coercion happens in the writer (`json.dumps(default=str)`), so the scope + itself must pass the object through untouched and still close the span. + """ + value = datetime(2026, 7, 29, 12, 0, 0, tzinfo=timezone.utc) + with failproofai_sdk.agent("a", session_id="s"): + with failproofai_sdk.tool_call("t") as call: + call.output = value + + assert events.types() == ["agent_start", "tool_use", "tool_result", "agent_end"] + assert only(events.entries, "tool_result")["output"] is value + + +def test_awkward_payload_survives_the_real_writer(tmp_path): + """The end-to-end version: through the actual JSONL writer, on disk.""" + import json + + from failproofai_sdk._events import EventNamespace + from failproofai_sdk._writer import EventWriter + + writer = EventWriter(flush_interval=3600) + namespace = EventNamespace(writer) + original = _runtime.event + _runtime.event = namespace + try: + with failproofai_sdk.agent("a", session_id="s"): + with failproofai_sdk.tool_call("t") as call: + call.output = datetime(2026, 7, 29, 12, 0, 0, tzinfo=timezone.utc) + writer.flush_now() + finally: + _runtime.event = original + + path = next((tmp_path / "events").glob("*.jsonl")) + parsed = [json.loads(line) for line in path.read_text().splitlines()] + assert [e["type"] for e in parsed] == [ + "agent_start", "tool_use", "tool_result", "agent_end", + ] + assert parsed[2]["output"] == "2026-07-29 12:00:00+00:00" + + +# --------------------------------------------------------------------------- +# `async with` parity — the same body, both syntaxes, byte-identical dicts +# --------------------------------------------------------------------------- + +def _scrub(entries): + """Drop the only fields that legitimately differ between two runs.""" + return [{k: v for k, v in e.items() if k not in ("timestamp", "duration_ms")} for e in entries] + + +def _sync_body(): + with failproofai_sdk.agent("planner", session_id="s-parity", goal="g", fw_node="n"): + with failproofai_sdk.agent("worker"): + with failproofai_sdk.tool_call("search", tool_call_id="tc-1", input={"q": "x"}) as t: + t.output = ["a", "b"] + + +async def _async_body(): + async with failproofai_sdk.agent("planner", session_id="s-parity", goal="g", fw_node="n"): + async with failproofai_sdk.agent("worker"): + async with failproofai_sdk.tool_call("search", tool_call_id="tc-1", input={"q": "x"}) as t: + t.output = ["a", "b"] + + +def _sync_failing(): + with pytest.raises(ValueError): + with failproofai_sdk.agent("planner", session_id="s-parity"): + with failproofai_sdk.tool_call("search", tool_call_id="tc-1"): + raise ValueError("nope") + + +async def _async_failing(): + with pytest.raises(ValueError): + async with failproofai_sdk.agent("planner", session_id="s-parity"): + async with failproofai_sdk.tool_call("search", tool_call_id="tc-1"): + raise ValueError("nope") + + +@pytest.mark.parametrize( + "sync_body,async_body", + [(_sync_body, _async_body), (_sync_failing, _async_failing)], + ids=["happy", "failing"], +) +def test_async_with_is_identical_to_with(events, sync_body, async_body): + sync_body() + sync_entries = _scrub(events.entries) + events.entries.clear() + + asyncio.run(async_body()) + async_entries = _scrub(events.entries) + + # `traceback` embeds the frame that raised, which differs by construction. + for entry in sync_entries + async_entries: + entry.pop("traceback", None) + + assert sync_entries == async_entries + assert sync_entries # not vacuously equal + + +def test_async_session_binds_and_unbinds(events): + async def main(): + async with failproofai_sdk.session("s-async") as sid: + assert sid == "s-async" + assert failproofai_sdk.current().session_id == "s-async" + + asyncio.run(main()) + assert failproofai_sdk.current().session_id is None diff --git a/sdk/python/tests/test_sdk.py b/sdk/python/tests/test_sdk.py index 38f5231ec..5f9c84ba7 100644 --- a/sdk/python/tests/test_sdk.py +++ b/sdk/python/tests/test_sdk.py @@ -556,10 +556,10 @@ def test_pending_dict_is_bounded(ns): ) assert len(ns._pending) == _PENDING_CAP # Earliest IDs were evicted. - assert _tool_key("s1", "a1", "call-0") not in ns._pending - assert _tool_key("s1", "a1", "call-49") not in ns._pending + assert _tool_key("s1", "call-0") not in ns._pending + assert _tool_key("s1", "call-49") not in ns._pending # Most-recent IDs are still tracked. - assert _tool_key("s1", "a1", f"call-{_PENDING_CAP + 49}") in ns._pending + assert _tool_key("s1", f"call-{_PENDING_CAP + 49}") in ns._pending # --------------------------------------------------------------------------- @@ -810,6 +810,44 @@ def test_configure_overrides_env_var(ns, mock_writer, monkeypatch): assert mock_writer.last()["environment"] == "staging" +def test_a_comma_in_configure_environment_is_refused_rather_than_dropped( + ns, mock_writer +): + # Ingest splits `environment` on commas to build its facets, so a line whose + # environment contains one is discarded WHOLE: 200 with + # `{"accepted":0,"skipped":N}`, the daemon deletes the delivered batch, and + # the run is simply never in the dashboard. Measured against the running + # stack before this check existed. `failproofaid` already refuses a comma in + # `collector.environment`; the SDK writes the same field and did not. + import failproofai_sdk._environment as env_mod + + with pytest.raises(ValueError, match="comma"): + env_mod.set_environment("prod,eu") + # And the rejected call must not have taken effect. + env_mod._environment = None + ns.agent_start(session_id="s1", agent_id="a1") + assert "," not in mock_writer.last()["environment"] + + +def test_a_comma_in_the_env_var_warns_and_falls_back_instead_of_raising( + ns, mock_writer, monkeypatch, caplog +): + # An env var is read lazily, inside `to_dict()` on whatever event happens to + # be next — raising there would take the caller's agent down from a line of + # telemetry, which a library in someone else's process must not do. Warn, + # and land the events under a visibly wrong environment rather than nowhere. + import logging + + import failproofai_sdk._environment as env_mod + + env_mod._environment = None + monkeypatch.setenv("AGENTEYE_ENVIRONMENT", "prod,eu") + with caplog.at_level(logging.WARNING, logger="failproofai_sdk"): + ns.agent_start(session_id="s1", agent_id="a1") + assert mock_writer.last()["environment"] == "dev" + assert "comma" in caplog.text + + def test_environment_is_reserved(ns): with pytest.raises(ValueError, match="Reserved field"): ns.agent_start(session_id="s1", agent_id="a1", environment="bad") diff --git a/sdk/python/tests/test_server_contract.py b/sdk/python/tests/test_server_contract.py index 86b87ed52..64f49abc8 100644 --- a/sdk/python/tests/test_server_contract.py +++ b/sdk/python/tests/test_server_contract.py @@ -24,7 +24,7 @@ import pytest -from failproofai_sdk import _events, _resolver, _schema +from failproofai_sdk import _context, _events, _resolver, _schema from failproofai_sdk._events import EventNamespace # ───────────────────────────────────────────────────────────────────────────── @@ -564,15 +564,61 @@ def test_a_non_string_session_id_is_refused_on_every_event(method, extra, bad): the realistic way in — an uninitialised variable, a lookup that missed. """ ns = EventNamespace(_Recorder()) - with pytest.raises(ValueError, match="session_id"): + with pytest.raises(TypeError, match="session_id"): getattr(ns, method)(session_id=bad, agent_id="a", **extra) @pytest.mark.parametrize("method,extra", ALL_METHODS, ids=[m for m, _ in ALL_METHODS]) def test_a_non_string_agent_id_is_refused_on_every_event(method, extra): + """`None` is no longer invalid — it means "resolve from scope" (below). + + `TypeError`, because the wrong TYPE was supplied — and because omitting a + required keyword argument was always a TypeError, so code catching one keeps + working now that identity is optional. + """ + ns = EventNamespace(_Recorder()) + with pytest.raises(TypeError, match="agent_id"): + getattr(ns, method)(session_id="s", agent_id=12345, **extra) + + +@pytest.mark.parametrize("method,extra", ALL_METHODS, ids=[m for m, _ in ALL_METHODS]) +def test_an_omitted_agent_id_resolves_rather_than_raising(method, extra): + """Omitting it is the ergonomic path the scopes exist for, not an error. + + With no `agent()` scope open it lands on `DEFAULT_AGENT_ID` — the convention + the skill already teaches — rather than raising. Inventing a *session* that + way would scatter one run across as many sessions as it has emit sites, so + only the agent has a default. + """ + recorder = _Recorder() + getattr(EventNamespace(recorder), method)(session_id="s", **extra) + assert recorder.entries[0]["agent_id"] == _context.DEFAULT_AGENT_ID + + +def test_an_unresolvable_session_id_raises_rather_than_emitting(): + """Nothing passed and nothing bound must be loud. + + Ingest skips an event whose `session_id` is not a JSON string and answers + `200 OK` with `{"accepted":0,"skipped":1}`, so emitting here would lose the + event with no error on either side — the exact failure the validation exists + to prevent, reached through the new ergonomic path instead of a bad argument. + """ ns = EventNamespace(_Recorder()) - with pytest.raises(ValueError, match="agent_id"): - getattr(ns, method)(session_id="s", agent_id=None, **extra) + with pytest.raises(TypeError, match="session_id is required"): + ns.agent_start() + + +def test_an_ambient_scope_satisfies_both_ids(): + import failproofai_sdk + + recorder = _Recorder() + ns = EventNamespace(recorder) + with failproofai_sdk.session("sess-x"): + with failproofai_sdk.agent("planner"): + ns.agent_start(goal="from ambient scope") + entry = recorder.entries[-1] + assert entry["session_id"] == "sess-x" + assert entry["agent_id"] == "planner" @pytest.mark.parametrize("blank", ["", " ", "\t\n"], ids=["empty", "spaces", "whitespace"]) diff --git a/sdk/python/tests/test_site_docs.py b/sdk/python/tests/test_site_docs.py new file mode 100644 index 000000000..727269442 --- /dev/null +++ b/sdk/python/tests/test_site_docs.py @@ -0,0 +1,198 @@ +"""The published docs site makes claims about this package, and nothing checked them. + +`docs/start/integrations/` on the site is a second copy of the integration guide, +maintained by hand alongside `sdk/python/docs/`. Two copies of the same claims is +exactly the shape that drifts, and it already had: the site named +`capture_content` for CrewAI and `session_id` for LlamaIndex, neither of which +those adapters read, and told readers to verify a Pydantic AI install by printing +`agent.capabilities`, which raises `AttributeError`. + +None of that produced an error for a reader — `instrument()` drops unknown option +keys by design — so only a test can catch it. + +Same shape as `test_spool_contract.py` and `fp-cli/tests/test_fp_home_contract.py`: +read the other side's source, skip when it is genuinely absent (an installed +sdist has no docs site), and fail when `FAILPROOFAI_SDK_REQUIRE_CONTRACT` says the +repository should be there. +""" +from __future__ import annotations + +import ast +import os +import re +import textwrap +from pathlib import Path + +import pytest + +import failproofai_sdk + +# sdk/python -> sdk -> repo root -> docs/ +_HERE = Path(failproofai_sdk.__file__).resolve().parent.parent +SITE = _HERE.parent.parent / "docs" / "start" / "integrations" + +REQUIRE = os.environ.get("FAILPROOFAI_SDK_REQUIRE_CONTRACT", "").strip().lower() in { + "1", + "true", + "yes", + "on", +} + +#: site page -> the adapter module it documents. +PAGE_ADAPTER = { + "langchain.mdx": "langchain", + "crewai.mdx": "crewai", + "llamaindex.mdx": "llama_index", + "pydantic-ai.mdx": "pydantic_ai", +} + + +def _page(name: str) -> str: + path = SITE / name + if path.is_file(): + return path.read_text(encoding="utf-8") + message = ( + f"{path} is missing. In an installed sdist that is expected. In the " + f"repository it means the docs site moved and these claims are now " + f"unguarded — re-point this test rather than deleting it." + ) + if REQUIRE: + pytest.fail(message) + pytest.skip(message) + + +def _adapter_options(module_name: str) -> set[str]: + source = ( + Path(failproofai_sdk.__file__).resolve().parent + / "integrations" + / f"{module_name}.py" + ).read_text(encoding="utf-8") + names = set(re.findall(r'options\.get\(\s*["\'](\w+)["\']', source)) + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.ClassDef) and node.name == "_Options": + names |= { + stmt.target.id + for stmt in node.body + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name) + } + return names + + +def _documented_options(text: str, adapter: str) -> set[str]: + out: set[str] = set() + for block in re.findall(r"```python[^\n]*\n(.*?)```", text, re.S): + try: + tree = ast.parse(textwrap.dedent(block)) + except SyntaxError: + continue + for node in ast.walk(tree): + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)): + continue + if node.func.attr != "instrument" or not node.args: + continue + first = node.args[0] + if isinstance(first, ast.Constant) and first.value == adapter: + out |= {kw.arg for kw in node.keywords if kw.arg} + return out + + +@pytest.mark.parametrize("page,adapter", sorted(PAGE_ADAPTER.items())) +def test_a_site_page_only_documents_options_the_adapter_reads(page, adapter): + documented = _documented_options(_page(page), adapter) + if not documented: + pytest.skip(f"{page} documents no instrument() options") + invented = documented - _adapter_options(adapter) + assert not invented, ( + f"docs/start/integrations/{page} documents instrument({adapter!r}, ...) " + f"options {sorted(invented)} that the adapter never reads. Unknown keys " + f"are dropped silently, so a reader gets no error and no effect." + ) + + +@pytest.mark.parametrize("page", sorted(PAGE_ADAPTER)) +def test_a_site_page_only_names_event_methods_that_exist(page): + named = set(re.findall(r"failproofai_sdk\.event\.([a-z_]+)\s*\(", _page(page))) + missing = {n for n in named if not hasattr(failproofai_sdk.event, n)} + assert not missing, f"{page} names event.{sorted(missing)}, which does not exist" + + +@pytest.mark.parametrize("page", sorted(PAGE_ADAPTER)) +def test_a_site_page_only_names_api_this_package_exports(page): + named = set(re.findall(r"failproofai_sdk\.([a-z_]+)\s*\(", _page(page))) - {"event"} + missing = {n for n in named if not hasattr(failproofai_sdk, n)} + assert not missing, f"{page} names failproofai_sdk.{sorted(missing)}, which does not exist" + + +def test_the_pydantic_page_verifies_the_capability_the_way_that_works(): + """`agent.capabilities` raises AttributeError. + + Pydantic AI merges the list you pass into a single `root_capability`, so the + verification snippet the page tells a reader to run has to go through it. + """ + text = _page("pydantic-ai.mdx") + assert "root_capability.capabilities" in text, ( + "the pydantic page must verify through `agent.root_capability.capabilities`" + ) + # Scoped to CODE, not prose: the page explains in words that + # `agent.capabilities` does not exist, and that sentence should stay. + code = "\n".join(re.findall(r"```python[^\n]*\n(.*?)```", text, re.S)) + assert not re.search(r"(? list[str]: + return [h.strip() for h in re.findall(r"^## (.+)$", _page(page), re.M)] + + +@pytest.mark.parametrize("page", sorted(PAGE_ADAPTER)) +def test_a_framework_page_follows_the_shared_spine(page): + headings = _headings(page) + missing = [h for h in SPINE if h not in headings] + assert not missing, f"{page} is missing sections {missing}; has {headings}" + + positions = [headings.index(h) for h in SPINE] + assert positions == sorted(positions), ( + f"{page} orders the shared sections differently: " + f"{[headings[i] for i in positions]}" + ) + + +@pytest.mark.parametrize("page", sorted(PAGE_ADAPTER)) +def test_a_framework_page_is_explicit_about_human_in_the_loop(page): + """Either it documents the pairs, or it says why there are none. + + Silence reads as an oversight, and the reader cannot tell whether the + framework has no HITL surface or we simply did not map it. + """ + text = _page(page) + assert "## Human in the loop" in text or "human-in-the-loop pair" in text, ( + f"{page} neither documents human-in-the-loop nor states that the " + f"framework has none" + ) diff --git a/sdk/python/tests/test_zero_dependencies.py b/sdk/python/tests/test_zero_dependencies.py index 0b1440658..eef089e0f 100644 --- a/sdk/python/tests/test_zero_dependencies.py +++ b/sdk/python/tests/test_zero_dependencies.py @@ -7,6 +7,15 @@ safe to add to an existing project without thinking, so it is a promise rather than a coincidence, and a promise needs a test. +The framework adapters under `integrations/` are the one exception, and they do +not weaken the promise. Each one imports the framework it adapts — there is no +other way to subclass its callback base class — but they are reached ONLY through +`instrument()`, which resolves them by string through `importlib.import_module` +at call time. So `import failproofai_sdk` still touches nothing outside the +standard library, and an adapter's import can only run in a process where that +framework was already installed and imported. Both halves of that are asserted +below, the second by launching a fresh interpreter rather than by reading source. + The declaration and the code are checked separately because they fail separately: a stray `import httpx` in a rarely-taken branch is an ImportError in the user's process at exactly the wrong moment, and an unused `dependencies` @@ -18,6 +27,8 @@ import ast import sys + +import pytest from pathlib import Path try: # Python 3.11+ @@ -42,10 +53,35 @@ ALLOWED = set(sys.stdlib_module_names) | {"failproofai_sdk"} +#: Adapters are allowed to import the framework they adapt. Nothing else is, and +#: this set is deliberately explicit rather than "anything under integrations/": +#: a new file added there gets scanned like core code until it is named here. +ADAPTER_IMPORTS = { + "langchain.py": {"langchain_core", "langgraph"}, + "crewai.py": {"crewai"}, + "llama_index.py": {"llama_index", "llama_index_instrumentation", "pydantic"}, + "pydantic_ai.py": {"pydantic_ai"}, +} + +#: Framework top-level names that must never appear in `sys.modules` after a bare +#: `import failproofai_sdk`. +FRAMEWORK_ROOTS = { + "langchain", "langchain_core", "langgraph", "crewai", + "llama_index", "llama_index_instrumentation", "pydantic", "pydantic_ai", +} + + def _module_sources() -> list[Path]: return sorted(p for p in PKG.rglob("*.py") if "__pycache__" not in p.parts) +def _allowed_for(path: Path) -> set[str]: + """What this module may import beyond the standard library.""" + if path.parent.name == "integrations": + return ALLOWED | ADAPTER_IMPORTS.get(path.name, set()) + return ALLOWED + + def _imported_top_level_modules(path: Path) -> set[str]: tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) modules: set[str] = set() @@ -70,7 +106,7 @@ def test_the_package_imports_only_the_standard_library(): """ offenders: dict[str, set[str]] = {} for path in _module_sources(): - external = _imported_top_level_modules(path) - ALLOWED + external = _imported_top_level_modules(path) - _allowed_for(path) if external: offenders[path.name] = external @@ -107,22 +143,42 @@ def test_no_runtime_dependencies_are_declared(): #: user's environment, and none of it is covered by the zero-dependency promise. ALLOWED_DEV_DEPENDENCIES = { "pytest": "the test runner", + "pytest-asyncio": "the adapters are half-async; their scopes are exercised " + "under `async with`, which needs an async test runner", "tomli": "tomllib's backport; Python 3.10 has no stdlib TOML parser, and " "test_zero_dependencies.py must not stop checking the manifest there", } +#: Extras that exist only to pull a FRAMEWORK in, mapped to the distribution each +#: is allowed to name. The adapter code always ships in the base wheel and lazy- +#: imports, so these gate nothing on our side — they are a convenience for users +#: who do not already have the framework. +FRAMEWORK_EXTRAS = { + "langchain": {"langchain-core"}, + "langgraph": {"langgraph"}, + "crewai": {"crewai", "onnxruntime"}, + "llamaindex": {"llama-index-core"}, + "pydantic-ai": {"pydantic-ai-slim"}, +} + + +def _named(spec: str) -> str: + """"crewai>=1.13,<2; python_version < '3.11'" -> "crewai".""" + head = spec.split(";")[0].strip().split("[")[0] + for op in ("===", "~=", "!=", ">=", "<=", "==", ">", "<"): + head = head.split(op)[0] + return head.strip() + def test_dev_dependencies_are_test_only_and_stay_out_of_the_install(): """`[project.optional-dependencies].dev` never reaches a plain install.""" manifest = tomllib.loads(PYPROJECT.read_text(encoding="utf-8")) extras = manifest["project"].get("optional-dependencies", {}) - assert set(extras) == {"dev"}, f"unexpected extras: {sorted(extras)}" + assert set(extras) == {"dev"} | set(FRAMEWORK_EXTRAS), ( + f"unexpected extras: {sorted(set(extras) - {'dev'} - set(FRAMEWORK_EXTRAS))}" + ) - # "pytest>=7" -> "pytest"; "tomli>=2; python_version<'3.11'" -> "tomli" - named = { - spec.split(";")[0].strip().split("[")[0].split("=")[0].split("<")[0].split(">")[0].strip() - for spec in extras["dev"] - } + named = {_named(spec) for spec in extras["dev"]} unexpected = named - set(ALLOWED_DEV_DEPENDENCIES) assert not unexpected, ( f"the dev extra grew beyond the test tooling: {sorted(unexpected)}. Each " @@ -167,3 +223,115 @@ def test_importing_the_package_does_not_touch_the_network_or_the_filesystem_eage f"__init__ gained module-level construction of {sorted(called - {'EventWriter', 'EventNamespace'})}. " "Import-time work is paid by every process that imports this package." ) + + +# ───────────────────────────────────────────────────────────────────────────── +# The adapters exist, so laziness is now the load-bearing half +# ───────────────────────────────────────────────────────────────────────────── + + +def test_importing_the_package_loads_no_framework(): + """The promise, asserted at runtime instead of inferred from the source. + + `integrations/` may import frameworks; what must stay true is that nothing + reaches it unless the caller asks. A fresh interpreter is used because this + test process has already imported half of everything — checking `sys.modules` + in-process would pass on the strength of nobody having imported LangChain yet. + """ + import json + import subprocess + + probe = ( + "import json, sys; import failproofai_sdk; " + f"roots = {sorted(FRAMEWORK_ROOTS)!r}; " + "print(json.dumps(sorted({m.split('.')[0] for m in sys.modules} & set(roots))))" + ) + result = subprocess.run( + [sys.executable, "-c", probe], capture_output=True, text=True, cwd=str(ROOT), timeout=60 + ) + assert result.returncode == 0, result.stderr + loaded = json.loads(result.stdout.strip()) + assert loaded == [], ( + f"`import failproofai_sdk` pulled in {loaded}. The adapters must stay behind " + "`instrument()`, which resolves them by string at call time — an eager " + "import here is an ImportError in every process that has not installed " + "that framework." + ) + + +def test_the_adapter_registry_holds_strings_not_modules(): + """`_REGISTRY` maps a name to a dotted path; importing it here would defeat it.""" + from failproofai_sdk.integrations import _REGISTRY + + assert _REGISTRY, "the adapter registry is empty" + for name, target in _REGISTRY.items(): + assert isinstance(target, str), f"{name} maps to {type(target).__name__}, not a path" + assert target.startswith("failproofai_sdk.integrations."), target + + +def test_instrument_on_a_bare_interpreter_installs_nothing_and_does_not_raise(): + """The realistic first call: no framework installed, `instrument()` anyway. + + It must return an empty result rather than raising — an observability call + that explodes because the user has not installed LangChain is worse than one + that does nothing. + """ + import subprocess + + probe = ( + "import failproofai_sdk as f; r = f.instrument(); " + "assert r == () or r == [], r; print('ok')" + ) + result = subprocess.run( + [sys.executable, "-c", probe], capture_output=True, text=True, cwd=str(ROOT), timeout=60 + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "ok" + + +@pytest.mark.parametrize("extra", sorted(FRAMEWORK_EXTRAS), ids=sorted(FRAMEWORK_EXTRAS)) +def test_a_framework_extra_names_only_its_own_framework(extra): + """Each one pulls in a framework and nothing else. + + An extra that quietly added a runtime library would put it in the + environment of everyone who typed `failproofai-sdk[langchain]`, which is the + zero-dependency promise leaking out through a side door. + """ + manifest = tomllib.loads(PYPROJECT.read_text(encoding="utf-8")) + specs = manifest["project"]["optional-dependencies"][extra] + assert specs, f"the {extra} extra is empty" + assert {_named(s) for s in specs} == FRAMEWORK_EXTRAS[extra] + + +def test_no_extra_refers_back_to_this_package(): + """A self-referential extra is how a package installs something else over itself. + + On public PyPI `agenteye` is the CLI, which is what made this a real hazard + in the SDK's previous home: `agenteye[langchain]` would have pulled the CLI + in on top of the SDK. The name is different here; the shape of the mistake is + not, and there is deliberately no `[all]` either — an extra that installs + four agent frameworks at once is a resolver problem handed to somebody who + wanted a telemetry library. + """ + manifest = tomllib.loads(PYPROJECT.read_text(encoding="utf-8")) + extras = manifest["project"]["optional-dependencies"] + assert "all" not in extras + for name, specs in extras.items(): + for spec in specs: + assert _named(spec) not in {"failproofai-sdk", "failproofai_sdk", "agenteye"}, ( + f"the {name} extra refers back to this package: {spec!r}" + ) + + +def test_every_framework_extra_has_an_upper_bound(): + """A ceiling on each, or a clean build a year from now silently stops working. + + The adapters subclass framework callback bases. A new major shifts that API, + the adapter stops receiving events, and it raises nothing while doing so — + the failure is an empty dashboard, not a traceback. + """ + manifest = tomllib.loads(PYPROJECT.read_text(encoding="utf-8")) + extras = manifest["project"]["optional-dependencies"] + for name in FRAMEWORK_EXTRAS: + for spec in extras[name]: + assert "<" in spec, f"{name}: {spec!r} has no upper bound" diff --git a/sdk/python/uv.lock b/sdk/python/uv.lock index 395bf7a76..c6427f215 100644 --- a/sdk/python/uv.lock +++ b/sdk/python/uv.lock @@ -1,151 +1,5069 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/4d/4a99fb425c5e0cad715eea7bd190aff46f38b959a0a2dadb993705d34b26/aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b", size = 765848, upload-time = "2026-07-23T01:52:08.217Z" }, + { url = "https://files.pythonhosted.org/packages/74/e8/43b85dc55b8e950dc644babe762add781319ea881b57b33d2cce12017d12/aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a", size = 517476, upload-time = "2026-07-23T01:52:10.846Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9e/73b582c4dbbc3c12ef4473822475effaabf1f934b56f14f5b03fe5d3a2af/aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5", size = 515334, upload-time = "2026-07-23T01:52:12.636Z" }, + { url = "https://files.pythonhosted.org/packages/79/03/e98c3c9e05a5bdf97defe5ff9169baba4f0ec9a901f2d60e0f060c2f051e/aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f", size = 1708830, upload-time = "2026-07-23T01:52:14.538Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2c/26e60b694844dfd2176c57f913a22d0cd6a16f9ff202cbda7580d0328b98/aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43", size = 1674012, upload-time = "2026-07-23T01:52:16.486Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/672df92e3172cd876aacfa97a952ac560877eb169384b2991ac5b273de4c/aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9", size = 1767015, upload-time = "2026-07-23T01:52:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c5/228dec7bfec1c373cc2217cdeb47d6456dcd7a13a4c55144930a75ae3851/aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8", size = 1858700, upload-time = "2026-07-23T01:52:20.08Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ff/cb36724e8c8d17f90ada567a9ff3efe1d6e9b549fba697a242aece180f21/aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479", size = 1714075, upload-time = "2026-07-23T01:52:22.071Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3a/296a4135c6366376263aeef54b15caca1f07676c2ae0c525d7832f2f808a/aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b", size = 1588234, upload-time = "2026-07-23T01:52:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/7d/81/9d5d853ef892dc066d1eb6db0e87a47348b920c1c879aa554612fdbd9d79/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d", size = 1677300, upload-time = "2026-07-23T01:52:25.861Z" }, + { url = "https://files.pythonhosted.org/packages/68/96/021d386ae32d9b26d4b88df2e794546232ff56bb6be952bf6be227c0bbc7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d", size = 1691501, upload-time = "2026-07-23T01:52:28Z" }, + { url = "https://files.pythonhosted.org/packages/29/9f/af66adce26a14af135c003cbd0f44ccaa68cebd30ff8ac99ca47fb4958f7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2", size = 1735113, upload-time = "2026-07-23T01:52:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/28c390d4c9851effe52ac25b5a2e1d92246acd00728b4fc7975dafb67484/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48", size = 1577486, upload-time = "2026-07-23T01:52:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/db/c2/00e23a1bf2abb70dd353f6987db7e7f2491d0261f7363997738c71c98f95/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f", size = 1751353, upload-time = "2026-07-23T01:52:33.688Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7d/d51a706a8cbfa57f0611127daf61ab3ae02ab8420b0407412079227d1c65/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32", size = 1698681, upload-time = "2026-07-23T01:52:38.167Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b0/90bd5cd9fdd9787cb4211d284d1fb8401339a933cb0227a15b71e789232f/aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e", size = 456733, upload-time = "2026-07-23T01:52:41.823Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/fe5b8f6a71ae112bc677163d0b0701bda5dc15005249582258ede0eb88c7/aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c", size = 480460, upload-time = "2026-07-23T01:52:43.905Z" }, + { url = "https://files.pythonhosted.org/packages/54/00/45e98b6645cd7f00a4b78b749ebd309094b0eaeb2d2e96157eadbc0d0050/aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb", size = 453479, upload-time = "2026-07-23T01:52:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aiosqlite" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +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 = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "banks" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "filetype" }, + { name = "griffe" }, + { name = "jinja2" }, + { name = "platformdirs" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/f0/ce5b3105a8551fdcedb509ab5340066247b3cd1b28e79f9296be2d6d3bf4/banks-2.5.0.tar.gz", hash = "sha256:fdd4fd54b84dbe31cb51a1173c960697c73d683a52fb0b1d1957a557a8d6fcc8", size = 194585, upload-time = "2026-08-08T15:54:16.548Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/9c/7dc7b15cecc03baa47d413b2599487b1fab270169b00feaa5e74978845f3/banks-2.5.0-py3-none-any.whl", hash = "sha256:8804c58f23e41a5aabe9ecfdf64235cdf1d5f3b16ad95ec2a54b6dc738dc1dfc", size = 38620, upload-time = "2026-08-08T15:54:15.505Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, + { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, +] + +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + +[[package]] +name = "cel-python" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-re2" }, + { name = "jmespath" }, + { name = "lark" }, + { name = "pendulum" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/4e/f821948a5bbd7a98a218720f831a62216f79a98e43b13d9ab2f98e37c5f8/cel_python-0.5.0.tar.gz", hash = "sha256:3eb0a619e8df0f338d0430cda01427a742e77e3c433a1c7c3ebd409cd804c45a", size = 13364027, upload-time = "2026-01-31T19:07:13.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/f8/38812adc3f787c2c2e8ba56f524185ed379656c10b40347a32796ba61c08/cel_python-0.5.0-py3-none-any.whl", hash = "sha256:d0f85008b89655c2bb18d797d2fa3f96f2ed80f4a3b43b0e8138c6646581e5f6", size = 84950, upload-time = "2026-01-31T19:07:11.821Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779, upload-time = "2026-08-03T21:19:15.602Z" }, + { url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178, upload-time = "2026-08-03T21:19:16.67Z" }, + { url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037, upload-time = "2026-08-03T21:19:17.705Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652, upload-time = "2026-08-03T21:19:18.735Z" }, + { url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422, upload-time = "2026-08-03T21:19:19.96Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444, upload-time = "2026-08-03T21:19:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742, upload-time = "2026-08-03T21:19:22.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054, upload-time = "2026-08-03T21:19:23.694Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489, upload-time = "2026-08-03T21:19:24.922Z" }, + { url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241, upload-time = "2026-08-03T21:19:26.214Z" }, + { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" }, + { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" }, + { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" }, + { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "chromadb" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "build" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "importlib-resources" }, + { name = "jsonschema" }, + { name = "kubernetes" }, + { name = "mmh3" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "onnxruntime" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "posthog" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "pypika" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/48/11851dddeadad6abe36ee071fedc99b5bdd2c324df3afa8cb952ae02798b/chromadb-1.1.1.tar.gz", hash = "sha256:ebfce0122753e306a76f1e291d4ddaebe5f01b5979b97ae0bc80b1d4024ff223", size = 1338109, upload-time = "2025-10-05T02:49:14.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/59/0d881a9b7eb63d8d2446cf67fcbb53fb8ae34991759d2b6024a067e90a9a/chromadb-1.1.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:27fe0e25ef0f83fb09c30355ab084fe6f246808a7ea29e8c19e85cf45785b90d", size = 19175479, upload-time = "2025-10-05T02:49:12.525Z" }, + { url = "https://files.pythonhosted.org/packages/94/4f/5a9fa317c84c98e70af48f74b00aa25589626c03a0428b4381b2095f3d73/chromadb-1.1.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:95aed58869683f12e7dcbf68b039fe5f576dbe9d1b86b8f4d014c9d077ccafd2", size = 18267188, upload-time = "2025-10-05T02:49:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/45/1a/02defe2f1c8d1daedb084bbe85f5b6083510a3ba192ed57797a3649a4310/chromadb-1.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06776dad41389a00e7d63d936c3a15c179d502becaf99f75745ee11b062c9b6a", size = 18855754, upload-time = "2025-10-05T02:49:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/80be82717e5dc19839af24558494811b6f2af2b261a8f21c51b872193b09/chromadb-1.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bba0096a7f5e975875ead23a91c0d41d977fbd3767f60d3305a011b0ace7afd3", size = 19893681, upload-time = "2025-10-05T02:49:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6e/956e62975305a4e31daf6114a73b3b0683a8f36f8d70b20aabd466770edb/chromadb-1.1.1-cp39-abi3-win_amd64.whl", hash = "sha256:a77aa026a73a18181fd89bbbdb86191c9a82fd42aa0b549ff18d8cae56394c8b", size = 19844042, upload-time = "2025-10-05T02:49:16.925Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + +[[package]] +name = "crewai" +version = "1.15.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiosqlite" }, + { name = "appdirs" }, + { name = "cel-python" }, + { name = "chromadb" }, + { name = "click" }, + { name = "crewai-cli" }, + { name = "crewai-core" }, + { name = "httpx" }, + { name = "instructor" }, + { name = "json-repair" }, + { name = "json5" }, + { name = "jsonref" }, + { name = "lancedb" }, + { name = "mcp" }, + { name = "openai" }, + { name = "openpyxl" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "pdfplumber" }, + { name = "portalocker" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "tokenizers" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/80/616fbc703f3e682dd7009c247d70b2e185a139f86e48c0b5a66be8f63adf/crewai-1.15.16.tar.gz", hash = "sha256:38f75f499a5e6a5dc78369a3f5384452b791bc4852a1ff38c342aca4e04e8264", size = 7885470, upload-time = "2026-08-14T00:08:52.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/55/29fc1a03ae190c0bc2f921dcf9226c0afa22483558a18bb8c6f0cbd1679d/crewai-1.15.16-py3-none-any.whl", hash = "sha256:a93ae2c78b42dacdb932c2e4bbba2efb108ac610c23cf5453d8d59000858665b", size = 1114637, upload-time = "2026-08-14T00:08:49.411Z" }, +] + +[[package]] +name = "crewai-cli" +version = "1.15.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "certifi" }, + { name = "click" }, + { name = "crewai-core" }, + { name = "cryptography" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "textual" }, + { name = "tomli" }, + { name = "tomli-w" }, + { name = "uv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/e4/eb56bf3c36485b6f6c51489d747d4f92750d3cadc19ecdb03c23dd026a74/crewai_cli-1.15.16.tar.gz", hash = "sha256:6d8f0c03bb05e88114b1a60e93aced09db2cc0a83f6811ec83f2e902fda14426", size = 227940, upload-time = "2026-08-14T00:08:56.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/b2/bbc5da7b20e15a5e67b92aed90a729a8e3175918adc626a763735d893d6a/crewai_cli-1.15.16-py3-none-any.whl", hash = "sha256:b98848f2cffe03fcefe69eeeb64e8dca8ec0fcc7f00437c419ad1972dd4a8c2c", size = 195493, upload-time = "2026-08-14T00:08:54.224Z" }, +] + +[[package]] +name = "crewai-core" +version = "1.15.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "cryptography" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "portalocker" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "rich" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/86/4d8273b80f41d7c7637ae0ada4d8ba78b540acf94735bdd0bf624ccb776d/crewai_core-1.15.16.tar.gz", hash = "sha256:c8d5548e223f32262243e91ee83b6976002616a26fc10951b3d5f1c05f10697f", size = 35445, upload-time = "2026-08-14T00:08:58.569Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/88/e954a1928421fd923b92c33cf22951d5e0dd2dc0c3fb129dd48dfeaa6b57/crewai_core-1.15.16-py3-none-any.whl", hash = "sha256:7a74e6ddbecd9df042dd459fd5e19a308b576958469bfa134ceb12dc6814106b", size = 40428, upload-time = "2026-08-14T00:08:57.231Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +] + +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "dirtyjson" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782, upload-time = "2022-11-28T23:32:33.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" }, +] + +[[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 = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +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 = "failproofai-sdk" +source = { editable = "." } + +[package.optional-dependencies] +crewai = [ + { name = "crewai" }, + { name = "onnxruntime", marker = "python_full_version < '3.11'" }, +] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +langchain = [ + { name = "langchain-core" }, +] +langgraph = [ + { name = "langgraph" }, +] +llamaindex = [ + { name = "llama-index-core" }, +] +pydantic-ai = [ + { name = "pydantic-ai-slim" }, +] + +[package.metadata] +requires-dist = [ + { name = "crewai", marker = "extra == 'crewai'", specifier = ">=1.13,<2" }, + { name = "langchain-core", marker = "extra == 'langchain'", specifier = ">=1.4.7,<2" }, + { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=1.2,<2" }, + { name = "llama-index-core", marker = "extra == 'llamaindex'", specifier = ">=0.14.23,<0.15" }, + { name = "onnxruntime", marker = "python_full_version < '3.11' and extra == 'crewai'", specifier = ">=1.14,<1.24" }, + { name = "pydantic-ai-slim", marker = "extra == 'pydantic-ai'", specifier = ">=2.0,<3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.3,<2" }, + { name = "tomli", marker = "python_full_version < '3.11' and extra == 'dev'", specifier = ">=2" }, +] +provides-extras = ["dev", "langchain", "langgraph", "crewai", "llamaindex", "pydantic-ai"] + +[[package]] +name = "filelock" +version = "3.32.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/64/a02e6765de08964ed371eca577870593245afc9dfac16d037de7c10d18e6/filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f", size = 218135, upload-time = "2026-08-13T16:00:05.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/8e/50f46a9c0ce8d2861a394c1347caae037ea0431d2f67d7feb151cbc4649a/filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09", size = 98901, upload-time = "2026-08-13T16:00:03.974Z" }, +] + +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx2" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/14/a188df294f013ec9cd97fc6b145f5427f89067bfb2c260fc3fb5c8d1fb34/genai_prices-0.1.3.tar.gz", hash = "sha256:62c30cddd6c2d2199d878d1a70521c3e37347cd9394446d107dc774a78ed3780", size = 92638, upload-time = "2026-08-15T00:10:31.771Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/cd/d94b47c26d6367e0b949edfe2da5a47fb74037e799a0edd5b825e049f2b9/genai_prices-0.1.3-py3-none-any.whl", hash = "sha256:a2603841429c843da91c987d9ef598c73bd940caf44e844ab046d551791c04bb", size = 96892, upload-time = "2026-08-15T00:10:30.595Z" }, +] + +[[package]] +name = "google-re2" +version = "1.1.20251105" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/60/805c654ba53d685513df955ee745f71920fe8e6a284faf0f9b9dc19b659c/google_re2-1.1.20251105.tar.gz", hash = "sha256:1db14a292ee8303b91e91e7c37e05ac17d3c467f29416c79ac70a78be3e65bda", size = 11676, upload-time = "2025-11-05T14:58:07.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/fb/36548d5d791d2d750dc6fc2ab87fbe50f0bcc054673e1cf64928908892a3/google_re2-1.1.20251105-1-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:88bd426c1904f3562049bf766301bbc4f7a4bcb8f61e92f8cc833faac1cf2a92", size = 483062, upload-time = "2025-11-05T14:56:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5d/25afc138821a1958940ee4a9bc83a87b59a6dbedd7ef0db4ee04b572a3b0/google_re2-1.1.20251105-1-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:a486dc10bb07f3c34b9908541368e21ab6d77972569427200db077126668fbf3", size = 514075, upload-time = "2025-11-05T14:56:51.871Z" }, + { url = "https://files.pythonhosted.org/packages/70/00/5303bb660b6f75a71f75dc818a35082c30508d4dd5477891f13e831f39e8/google_re2-1.1.20251105-1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:a9aa02dc1345f0889c6ce1365d5f93d5b161b512f4c6df3cfadf3298493fb678", size = 484069, upload-time = "2025-11-05T14:56:53.479Z" }, + { url = "https://files.pythonhosted.org/packages/55/d3/8d11005db3000128055f6d3868a3216dd639721040eb988b3eccce852bc0/google_re2-1.1.20251105-1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:032160ad8c05739370813bcb15099854cd50faa933e0fe9607a2380659c750df", size = 515556, upload-time = "2025-11-05T14:56:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/21/36/c7d3c8dd7578badb53b929f5c8cc78bbbec23163029a15fdce2dfabf78f4/google_re2-1.1.20251105-1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:39a7013477c8778b1ddcc0d43eff0ee4a0f66b76c9db21f9e7b7d1f74852633f", size = 481738, upload-time = "2025-11-05T14:56:56.429Z" }, + { url = "https://files.pythonhosted.org/packages/61/c3/2199a9edefa1ffea59e5e54ebca34a126e0a2c5b4b2c73db9c5b97b9895d/google_re2-1.1.20251105-1-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:f886c88d56233483c5fd5ed1234e7e72389b8331250100983443fa30855deb63", size = 507751, upload-time = "2025-11-05T14:56:58.035Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/e9a9fa5fd3b309c76262fd8642346b62235f7a9b7590563403ef427a366b/google_re2-1.1.20251105-1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8beddf48857fd3767c553f0be7414a7a483f9b6374c91c02474a616fc7f5c5b3", size = 572738, upload-time = "2025-11-05T14:56:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/65/d3/4aad2f11e635709c326a1c34bff59c879dab5c2ff720dbcd275c61c3ea56/google_re2-1.1.20251105-1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a319dcb37b069d72d968862335197f460803b3a35f99445ea805f69fac58759", size = 588959, upload-time = "2025-11-05T14:57:00.675Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/ce78b34800b966fc7c4abf2f40e71ece39c1485b57a283bcffae054a5aa3/google_re2-1.1.20251105-1-cp310-cp310-win32.whl", hash = "sha256:420fe037ad77ab3d1a280c6823985b89160896f66ce601a3923d020690a1f9b4", size = 432828, upload-time = "2025-11-05T14:57:01.985Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4e/d381ebce2d14b381379485845f884d8c7b491196fed62c68932a4e5fef69/google_re2-1.1.20251105-1-cp310-cp310-win_amd64.whl", hash = "sha256:462dfcf147d0f54d0c93a69c361225119a4987c3b0ecd77f0e21ad9ba8bf180e", size = 490179, upload-time = "2025-11-05T14:57:03.278Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4d/203a08dab1bdb5c83b46dd424c01a789ecb5a37dbc80f33d016bd116a9d7/google_re2-1.1.20251105-1-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:329efa209ea7baa44f0facf0402fa34e655dc97fdeb10d0b83fc06354f5575fd", size = 483717, upload-time = "2025-11-05T14:57:04.808Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/466026b43ff5c7d740f5ede090992ec63b60d1810ab14fe35dfc00677e0a/google_re2-1.1.20251105-1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:aa2ad5f6f48921ec137a7b7f1b1da903ddef8627a2dc30bc878a9a69d9925719", size = 515547, upload-time = "2025-11-05T14:57:06.013Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6a/c6c9fdb00c98990e4f7a6cd650e209d7b5d2754ca0404b72c69ac9909a69/google_re2-1.1.20251105-1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ac1cb2526cc88f050a0661fc7245ad009ee454bddc541b2e653f1d007585000d", size = 485396, upload-time = "2025-11-05T14:57:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f6/529c44f607c47f96cfa29c1fe3a690fe75b2fdb48e9b0d6b54e5f0a75e59/google_re2-1.1.20251105-1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:50c7205182ad66c23c07abe8072f720ca2f7d595b61e28fd9b63623614f9afd6", size = 517150, upload-time = "2025-11-05T14:57:09.376Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/ccc07860e31ab81965c63f9ed4eb69ea0d3449a9b4e1610f71883694bbe8/google_re2-1.1.20251105-1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:4cb5acee61e35772503b8b1db3c592a46b8e6a9bc0ab54d7d6233654ea2bf93d", size = 482807, upload-time = "2025-11-05T14:57:11.057Z" }, + { url = "https://files.pythonhosted.org/packages/bd/43/5fb20d16664457f61670bdd95f39039d43ee8b7732511c688e2f322a4317/google_re2-1.1.20251105-1-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:1617097d63620c2d46bdfc0e48f24f66cd341664fc75718636d234f67473fe7f", size = 508839, upload-time = "2025-11-05T14:57:12.338Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f2/6e470338271e164dd3c5e508876f99aec3ed23bf419c7d54a5672fd5b05f/google_re2-1.1.20251105-1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a5610b26742b90cb1d64ead2b16fe0e3bd7e67add03fd3779cd1b85e401661", size = 573718, upload-time = "2025-11-05T14:57:13.635Z" }, + { url = "https://files.pythonhosted.org/packages/91/21/4566fc344c21cf3c49082d13ddab785994b5e3b8b7fd4631242538f698a2/google_re2-1.1.20251105-1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03156291269f145eccddff63118f2df02d395792f51fc039f09955818943815a", size = 590749, upload-time = "2025-11-05T14:57:14.864Z" }, + { url = "https://files.pythonhosted.org/packages/94/19/5981fb798bb8d08933b815b1fd9e55d179c380b9d8c21a49197b9b7c5967/google_re2-1.1.20251105-1-cp311-cp311-win32.whl", hash = "sha256:54f51762b51dc238eceddf49b56cc2b64594fe72d9328c1c39d615aa990e1f87", size = 434066, upload-time = "2025-11-05T14:57:16.22Z" }, + { url = "https://files.pythonhosted.org/packages/49/e5/f83053a36cfc4762d843748e4f7a9c1141937dcf74cd6fc3f4598292dda3/google_re2-1.1.20251105-1-cp311-cp311-win_amd64.whl", hash = "sha256:f5f856ff5036a8f22b3bad57f376d4e3b97b59b64f311bdb1f83c8dabded2492", size = 491025, upload-time = "2025-11-05T14:57:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/56/be/4315c3b38f42f9a2888fa76260545c98547502f1c35aa63a672d39011b2e/google_re2-1.1.20251105-1-cp311-cp311-win_arm64.whl", hash = "sha256:913864f97de4151eaa8bb7746ca230fd193656501e07fb658ce2cd46d4f6efcc", size = 642194, upload-time = "2025-11-05T14:57:19.374Z" }, + { url = "https://files.pythonhosted.org/packages/67/20/73b487538e9107c2fd96aed737e3f3890dfce3e292622e4ffb2f9c810ee5/google_re2-1.1.20251105-1-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b30f09b4d63249c72e65ccae4cbf6b331b48c22fc7cb439f1d85f347b9d07ceb", size = 485591, upload-time = "2025-11-05T14:57:20.961Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9a/ca3a993bdb5dc6d5b2616b9657b2872a83d1827f8bd3ab50cd629eb751c7/google_re2-1.1.20251105-1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:9a77892c524b8bdf3d47d7cad1cc2ac3a0108bdd65007ef4c02888fa46baf8ee", size = 518780, upload-time = "2025-11-05T14:57:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/df/37/b2e367987371514253ec9e514637f457deaacb7acc1c900814f3a6421e0f/google_re2-1.1.20251105-1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a3ac51b28cbf25c100dfd8849212d878d7005d1d4a7e129a10789043c56b6021", size = 486966, upload-time = "2025-11-05T14:57:24.575Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/1db6742943c0ac254bfb7d8a37a5d3f73f016a65cfa1f84fe3a0451820f6/google_re2-1.1.20251105-1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:9f7158afc9825ac2654c6561aea94a1f7edb5b5b88e6e3639bb80bb817d102ac", size = 520225, upload-time = "2025-11-05T14:57:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0a/0747c92dbebe2c09a26bd7386d372b5c5a9926236b4f3d69bb8f15db05cb/google_re2-1.1.20251105-1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:5320da07dc3b7ac7f407514f42ac17d67e771ac7c7562d449571185e6fb601b2", size = 482943, upload-time = "2025-11-05T14:57:27.353Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/6bfc6838bb6cb561824ac03deeab2bd11d5d9a93505f536c8fa2f6bd46c4/google_re2-1.1.20251105-1-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:5a4e5785bc30d52ce655d805b07ad2d8a4905429a5f690ae9c2f1caa76665709", size = 510384, upload-time = "2025-11-05T14:57:29.139Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/6add090c917ee39f6f0be753037cafceb3bad904b424efc155fb38082635/google_re2-1.1.20251105-1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b7a3b90f747130310d4b3b8e19ebb845d0d97c1deb63b36f76c7242dacbd736", size = 572446, upload-time = "2025-11-05T14:57:30.495Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1c/8b1ccbeade96a21435d55b5185cd6d9b2ceab5a9af998a4d9099e0540759/google_re2-1.1.20251105-1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:809c5fa5d08279413b29c2e2c5c528e85cd94a0e0fd897db595a0c09eeee2782", size = 591348, upload-time = "2025-11-05T14:57:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/62/cf/7bdd7a1ae7828b613011da808eafec4da3132f43c3be6af5e0bd670ebe8b/google_re2-1.1.20251105-1-cp312-cp312-win32.whl", hash = "sha256:d8424e63a9ec0fe5bde03d97876b2431f8a746af33eb475fa1ae39144bd05b2a", size = 433787, upload-time = "2025-11-05T14:57:33.071Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/5dd951c35acaabfe87c67228b9af2cdcd7779d9167edbe6b9094b8a8e529/google_re2-1.1.20251105-1-cp312-cp312-win_amd64.whl", hash = "sha256:062313c309f93dfeb6966372f4c446580e98879133ec155522eea8aaf568a5cd", size = 491726, upload-time = "2025-11-05T14:57:34.39Z" }, + { url = "https://files.pythonhosted.org/packages/60/8d/c1afd29fc2cb475fd4c634f3d3c8099c0efb662362c10b27a9eaf11c9357/google_re2-1.1.20251105-1-cp312-cp312-win_arm64.whl", hash = "sha256:558f144b26a9555ae4e9467cc3aa3299a8ce13217f328b21ae326ca0633be19b", size = 642673, upload-time = "2025-11-05T14:57:35.693Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/c441722196598fc3de0f654606ad9975a968c71dc27f516b5a4c9ebb94fd/google_re2-1.1.20251105-1-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:9f3cf610e857a7d6f02916cf2b7fc159a5429b8bcb23164500d46e5e233f2924", size = 485549, upload-time = "2025-11-05T14:57:36.939Z" }, + { url = "https://files.pythonhosted.org/packages/ea/87/cf588255e5ada1dfb555cc96de35be78438bb0b6faba64df5fe91cecc224/google_re2-1.1.20251105-1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:a21c2807bf4d5d00f206a4ecb3b043aad674e28c451b697b740280f608872078", size = 518840, upload-time = "2025-11-05T14:57:38.115Z" }, + { url = "https://files.pythonhosted.org/packages/0d/39/da66e4ca9be0c51546efc6fb39cf1683c4be8245d8199cb54a9808e8d5fa/google_re2-1.1.20251105-1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8314144eefeee7b88b742081c2038418f677e63901039ca9dbfbc0c5bb6d2911", size = 487037, upload-time = "2025-11-05T14:57:39.467Z" }, + { url = "https://files.pythonhosted.org/packages/75/dd/24ba65692dd58dca6ff178428551f4e9b776d1489a1251f5c8539e598baa/google_re2-1.1.20251105-1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:28a46be978e53c772139d0f5c9ba69f53563fcdd4225407e4d34d51208b828f1", size = 520285, upload-time = "2025-11-05T14:57:40.666Z" }, + { url = "https://files.pythonhosted.org/packages/61/12/cfdbb92bed24af6474970a75a26145c424f98cfbcc633fdd185985f0efe0/google_re2-1.1.20251105-1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:83292e23963aa1b219d5f64a65365b0880448a6a060276027b55270bc5b18c7e", size = 482981, upload-time = "2025-11-05T14:57:41.928Z" }, + { url = "https://files.pythonhosted.org/packages/97/bf/5fc32ded9279e69a87b88d7261e7e77e2e26325d4e27ca1303a3215e430a/google_re2-1.1.20251105-1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:1920b15dc9b1bdfeca5aa2c60900373c6f27cd1056d53cd299456ea5540a6fff", size = 510366, upload-time = "2025-11-05T14:57:43.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/71/f927ddc7aef1b8d7ccc8a649c335d311f29f3dea658209e30e37720e4891/google_re2-1.1.20251105-1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b1458d9ca588124cd61aa1bf5388a216e1247e7d474f8e5e1530498044f5c87", size = 572390, upload-time = "2025-11-05T14:57:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8c/23075e589038284c9487f41cde531d35873f9da622fb4ac7d1d97bd9086e/google_re2-1.1.20251105-1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a52cb204e49d20cdbb66faf394d57f476e96c39c23a328442ab0194fc6bd1a2b", size = 591386, upload-time = "2025-11-05T14:57:45.713Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/858453ef689f6b9895cd02b466836a9d1a6e4ba535d1a275b01bf73baa1d/google_re2-1.1.20251105-1-cp313-cp313-win32.whl", hash = "sha256:67c5c73d7ebcf3f0e0a3b528b41bd8c6c04900f1598aebf05bbdf15a06cf5f9a", size = 433807, upload-time = "2025-11-05T14:57:46.92Z" }, + { url = "https://files.pythonhosted.org/packages/08/24/6ea87fe682e115ffd296e91eb5c5a266349d1ee8414ce8ece3f99ec1ac84/google_re2-1.1.20251105-1-cp313-cp313-win_amd64.whl", hash = "sha256:0bcba63ad3ea8926fb0c71bb5044e33d405bb9395f5b5444393cd5f28f0bf6d3", size = 491734, upload-time = "2025-11-05T14:57:48.304Z" }, + { url = "https://files.pythonhosted.org/packages/34/85/32ba71b06f3cf5f9856ae95b3d6463b971742453631a5ae2c5be338ea377/google_re2-1.1.20251105-1-cp313-cp313-win_arm64.whl", hash = "sha256:64ee189ea857f2126c5e42073cfa9b03e9f4cbaf073edbedb575059074841aa0", size = 642654, upload-time = "2025-11-05T14:57:49.602Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7f/7eb238bdcd06182b5f427afd305cf413b7cf4ea71047308bbf35912cf923/google_re2-1.1.20251105-1-cp314-cp314-macosx_13_0_arm64.whl", hash = "sha256:cc151cf6a585d9ebe711da32b23683fcff40f78db8c8587c7f4b209ef4658809", size = 484719, upload-time = "2025-11-05T14:57:51.326Z" }, + { url = "https://files.pythonhosted.org/packages/6d/62/eed28eab67f939f4b9383c47b1db11638ade6ac30785c15cb960de85ba43/google_re2-1.1.20251105-1-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:7e2186d2c90488c1e11895343941f35ca2f58e9ba6c6b034fd531abe22ef77cc", size = 517698, upload-time = "2025-11-05T14:57:52.597Z" }, + { url = "https://files.pythonhosted.org/packages/f7/16/a1e6768513f788bf9c67a1cfe379ef34a793983eee46e4b653e42b558b78/google_re2-1.1.20251105-1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:41be22359c3dceb582937739b4365dd8e279de24ad0a5b10e653503abaff2ed7", size = 486421, upload-time = "2025-11-05T14:57:53.852Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fc/7a97ffd36d451e5a8bfaff2f9022b14807795d588f98227ff96e8da99856/google_re2-1.1.20251105-1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f3168d7bbac247c862ea85b2f3c011d3a04bedcb6892b37f14d488f4133b206e", size = 519037, upload-time = "2025-11-05T14:57:55.078Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ee/8b6f7d94bb689dafdf60de8dd8f8f6296ad40d4d15c933fcda4da7a3a06b/google_re2-1.1.20251105-1-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:79ce664038194a31bbcf422137f9607ae3d9946a5cff98cf0efbeb7f9411e64b", size = 483373, upload-time = "2025-11-05T14:57:56.297Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a6/16a09e03d1de128f821869e4252688c21319f5017d9209f4d0e71ea5c951/google_re2-1.1.20251105-1-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:0476b07421b8882b279d5ceb5b760c15c62d581ded95274697fc1227e3869ee6", size = 510167, upload-time = "2025-11-05T14:57:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/c4/9d/213dce5de401527369fb5af11096b18c06001d9eb71f3318fe5eba1ec706/google_re2-1.1.20251105-1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85feec3161ffdc12f6b144e37a2f91f80b771c72ffadde60191e89a49f6d7e81", size = 573176, upload-time = "2025-11-05T14:57:59.211Z" }, + { url = "https://files.pythonhosted.org/packages/03/be/a8def96aa4a80b233e105767d22e3de961dcde5a04f0a05cb4f3ddb4df78/google_re2-1.1.20251105-1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7bfaa2cf55daf0c5c650e68526bb20b61e37d7f3ae53f6893013acc1c91c116", size = 591483, upload-time = "2025-11-05T14:58:00.416Z" }, + { url = "https://files.pythonhosted.org/packages/14/ea/144bbc4b9359da89aec07b4c2a91a6bfe7119914885386577c665b07bb01/google_re2-1.1.20251105-1-cp314-cp314-win32.whl", hash = "sha256:214c1accdc60fff9ce1bf812b157147ca361844f496ed9e0d5f357b0e562ced8", size = 433773, upload-time = "2025-11-05T14:58:01.594Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/74e301211699f1b650ba7690a3e4e52146ac4266fcd62f3ea0a945b9eda4/google_re2-1.1.20251105-1-cp314-cp314-win_amd64.whl", hash = "sha256:6d4d5fdadd329a2ed193463899d00ef2fd126172f36a4c01c9def271f19801b6", size = 491893, upload-time = "2025-11-05T14:58:02.969Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d1/4adcfcb9c95e3d064c9f7aaf6cb3a4fc842d86115014b9d4094db4d465b5/google_re2-1.1.20251105-1-cp314-cp314-win_arm64.whl", hash = "sha256:1d27f3a2a947ec1f721d0f14f661108acfd4f4d34f357ce28db951cc036656e5", size = 643093, upload-time = "2025-11-05T14:58:05.761Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/73/74bcab964c9a7a61f2bb71e8179b0f13e6fa98f7ce00fd168aab291e4a2e/googleapis_common_protos-1.75.1.tar.gz", hash = "sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071", size = 150967, upload-time = "2026-08-06T06:24:51.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/51/186c02b8549b69ccda44429cf6ff5081e4b61a602ddfe6a8020d1be31d1b/googleapis_common_protos-1.75.1-py3-none-any.whl", hash = "sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79", size = 300626, upload-time = "2026-08-06T06:23:46.696Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/67/07ecd6d85c0f253363cbc4ac9e7dd048ca571a267fbccfea084d6009ac3b/greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667", size = 292971, upload-time = "2026-08-10T13:28:09.837Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/426733bc24247ee17bb76df90f550c299ff5f8572b2bdd7cacb5c53fd994/greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b", size = 609290, upload-time = "2026-08-10T14:14:32.273Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dc/17d3a5acceb2fd0bbc9682a228117b719cdfb68085e6dbb33fa325755f27/greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502", size = 622650, upload-time = "2026-08-10T14:27:22.004Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/b31e6bd6adb8f80492efb6a47e8f9cd0e7335db5aac2795b1876d5afcfee/greenlet-3.5.5-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c", size = 629560, upload-time = "2026-08-10T14:30:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/51/91/00b3c0566316c6f383ea16ee05388ec4f57f6e0afa49e72ae471eda8c47f/greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56", size = 622819, upload-time = "2026-08-10T13:40:46.562Z" }, + { url = "https://files.pythonhosted.org/packages/18/5e/58c561efde575c4ca070030e2e0513e8d1b07b76d8a2d84a9bcef1becd42/greenlet-3.5.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638", size = 425489, upload-time = "2026-08-10T14:29:59.768Z" }, + { url = "https://files.pythonhosted.org/packages/3c/78/52de4f7ac9152ad1dbc8f437895c1e573d30ee1e1427e0f91a280e2417b6/greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0", size = 1582164, upload-time = "2026-08-10T14:15:02.645Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ff/c3855a00c2417e8f61c7b9f0bc4f8f599c6928f5c15681ad251e78a91e05/greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4", size = 1648807, upload-time = "2026-08-10T13:40:27.471Z" }, + { url = "https://files.pythonhosted.org/packages/70/21/d29ff6a79dbd1ec1fe74c155d25d4c5a85e221d6b9ffc2cc7a709e7c8c39/greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc", size = 322832, upload-time = "2026-08-10T13:28:10.85Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/39/82/5c873dbb4fb001d22fbbd50e80d4c1b0181ddae106856132160f84b94e88/greenlet-3.5.5-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c", size = 633329, upload-time = "2026-08-10T14:30:06.062Z" }, + { url = "https://files.pythonhosted.org/packages/51/2d/f2c928218ac52f26d7a2c188c171d1b7e728b23782cb3347e7b4fce1493a/greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864", size = 624562, upload-time = "2026-08-10T13:40:48.064Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/f7df98e72a8eb4a7edfec5a08d6d1a4ab53a52c95ba0b2ea6c10b8dd9bd0/greenlet-3.5.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f", size = 428145, upload-time = "2026-08-10T14:30:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/92fc51d5d35912f4f06eec037ba347985defd0be47463a010a325634d9d2/greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db", size = 1584909, upload-time = "2026-08-10T14:15:04.343Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/ed98b80ac5738c149a5258544843c45601ade1fd70f61740cdaead6351b3/greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39", size = 1651184, upload-time = "2026-08-10T13:40:28.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/be/b582ceb80cefdf9d8da34078714e4b12b3d16f509dee0f65e40a5cc8fc7d/greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53", size = 323280, upload-time = "2026-08-10T13:26:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/4d/18/5313c4c58598c38b0373c013e4ff2b3e6d258aaaa338f373335ebecdaddd/greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5", size = 307785, upload-time = "2026-08-10T13:28:34.874Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, + { url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, + { url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, + { url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, + { url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, + { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, +] + +[[package]] +name = "griffe" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffecli" }, + { name = "griffelib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/a0/927cf9416f527f623f4ccd9220337fca0cd85303b7e82adeab1e53669cd3/griffe-2.2.0.tar.gz", hash = "sha256:9c0dd9a7feda9e169d783507d777c6fb2f2fc2919b139747a5b4f5357517a611", size = 246229, upload-time = "2026-08-16T14:04:57.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/51/c9f9cb144e2bd21cbcb6ebd7978ecb5b9f5a375e78313744158338c8039c/griffe-2.2.0-py3-none-any.whl", hash = "sha256:db20672b31c3de2a1af5a18a3b3d8c186ccc2425e10290486b4f0364d25929e9", size = 5070, upload-time = "2026-08-16T14:04:51.971Z" }, +] + +[[package]] +name = "griffecli" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "griffelib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/75/3cf2fb1ae21fb55a3b917b45d24426a620ee916f4bdbd03f350c69d3d892/griffecli-2.2.0.tar.gz", hash = "sha256:b9e763131218eb19887ed52de20fe944252d811bfb4a3e339874908a588f0044", size = 58066, upload-time = "2026-08-16T14:04:55.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/83/04652213d82afeecc8e8a31231eeb87cc484dcc055fd96ecb0bfd9860405/griffecli-2.2.0-py3-none-any.whl", hash = "sha256:8ebb7ae1dcc2617f5d39f00e4c238dd6b5cd0b13157b0ce22c1ea4ca8b4c5d1d", size = 11480, upload-time = "2026-08-16T14:04:53.14Z" }, +] + +[[package]] +name = "griffelib" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, +] + +[[package]] +name = "grpcio" +version = "1.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24", size = 13428824, upload-time = "2026-07-23T15:20:37.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/fd/655c8a773d728bc3c93fb4713ae4bf79ffc75996f86fb78b2974c8e1dfbd/grpcio-1.83.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8", size = 6334247, upload-time = "2026-07-23T15:18:53.099Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9a/1ce5760d35a04a992006dd2f79afff2db548f93ee7426fa95c9f1fc90c61/grpcio-1.83.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727", size = 12168650, upload-time = "2026-07-23T15:18:56.348Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ab/bbcb5be0a1a6cb21f036e2afdd4f7a70147cfb7a7b42648a310d7c43acfc/grpcio-1.83.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf", size = 6916899, upload-time = "2026-07-23T15:18:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d2/4c27977ecb3b3f9f363b93f570e001cb24ef264a9a907d7fd0f949ed59f0/grpcio-1.83.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9", size = 7648761, upload-time = "2026-07-23T15:19:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/23/49/0c823a7627ff2e69a61e4a53c4edf215272892fc2c47c6431f033d46f4cc/grpcio-1.83.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4", size = 7074920, upload-time = "2026-07-23T15:19:02.293Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ce/963f01ff7c789a76909c9691b704112e02ca1e11c10405cd99c2bd7c40f1/grpcio-1.83.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb", size = 7598046, upload-time = "2026-07-23T15:19:03.921Z" }, + { url = "https://files.pythonhosted.org/packages/eb/de/1ce6bdefc847a7973040d10cebc8996c653a2a687c0a4da8d05dcab4e397/grpcio-1.83.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a", size = 8634792, upload-time = "2026-07-23T15:19:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8b/7fe6a73895e3bdd788101d1276e48e0d262ebb165afacec1ec4efebcd785/grpcio-1.83.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40", size = 8000286, upload-time = "2026-07-23T15:19:07.739Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b0/9a779de2bcda8722501a056fad1bec3d1117977af0c080ab1fc0655fdf35/grpcio-1.83.0-cp310-cp310-win32.whl", hash = "sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03", size = 4404616, upload-time = "2026-07-23T15:19:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8e/ce9a23590cac33a6c24e6386cc0ffc55821cc13212acc822e98f00a67161/grpcio-1.83.0-cp310-cp310-win_amd64.whl", hash = "sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57", size = 5162304, upload-time = "2026-07-23T15:19:11.467Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f6/3b781cd07a715ea5f5125ae264226e7fc4d87603d6d3955022cabfdc5da2/grpcio-1.83.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f", size = 6338720, upload-time = "2026-07-23T15:19:13.177Z" }, + { url = "https://files.pythonhosted.org/packages/21/cc/d14833d15d5984e366f1b027fa78bd038c9b028c66880bffb0f5a4d25ee2/grpcio-1.83.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223", size = 12178773, upload-time = "2026-07-23T15:19:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/6b/98/8acbb416544e7871132d8e42a07ed70c802d70e6a16c6009e505a34d32a4/grpcio-1.83.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0", size = 6921203, upload-time = "2026-07-23T15:19:17.824Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/0fdbfaf4fc54e5c88f6bce4008a065092fe7fbc4460eb5617ae8b20fd505/grpcio-1.83.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d", size = 7648508, upload-time = "2026-07-23T15:19:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ea/107b9dbb2ed3ad14dd774fd3dde7d29ff9938a6c198654becb2c3a0e9a6a/grpcio-1.83.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9", size = 7079466, upload-time = "2026-07-23T15:19:21.478Z" }, + { url = "https://files.pythonhosted.org/packages/3b/06/9fa9941089e6fae83b060b6ce61c1e81053e52decae43197245f45e07d36/grpcio-1.83.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745", size = 7605583, upload-time = "2026-07-23T15:19:23.74Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/f10fb56062dc2771c630827a82d9ad0ecd05cad572ea3b08d49f6631680a/grpcio-1.83.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617", size = 8637810, upload-time = "2026-07-23T15:19:25.536Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/f84927258f6a1b6ea6dea661fdc6de859b35e560c96f3012d15ccd39f85e/grpcio-1.83.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969", size = 8008021, upload-time = "2026-07-23T15:19:27.863Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/cdf72161397ccd29d4ca2192f641524536c9cf54ad948c9dd0e0e01138fa/grpcio-1.83.0-cp311-cp311-win32.whl", hash = "sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45", size = 4404376, upload-time = "2026-07-23T15:19:30.137Z" }, + { url = "https://files.pythonhosted.org/packages/df/ed/e0ffeb4c848699c194dc9fb6a29ab29bcb2b6aac8c416bf18c51bfe8242c/grpcio-1.83.0-cp311-cp311-win_amd64.whl", hash = "sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49", size = 5164469, upload-time = "2026-07-23T15:19:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/15/2b/51e32514a4e9b715375c99721aadff0f24164cc2049b8269eda4de82a814/grpcio-1.83.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930", size = 6303167, upload-time = "2026-07-23T15:19:33.78Z" }, + { url = "https://files.pythonhosted.org/packages/39/33/b5b50fc2c6fbe350e04814047bb2d409feec7b36ef8b170254c050e06bc0/grpcio-1.83.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da", size = 12160538, upload-time = "2026-07-23T15:19:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5f/734e72e7b9f79bcf0b2c270b8d3bca0e4ebb97a27a50d06240b145f6d41e/grpcio-1.83.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16", size = 6869310, upload-time = "2026-07-23T15:19:38.607Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/a1735f215b2a5cd43c38b79eac072ad197e61be9829905b6b29550abd0db/grpcio-1.83.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf", size = 7613472, upload-time = "2026-07-23T15:19:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/b2/78/c9e81f806ac704b6b145cb01628db398985b1f8dfdc10e23b55fb0902b3d/grpcio-1.83.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd", size = 7040616, upload-time = "2026-07-23T15:19:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ba/94cd5af859876049d340480acbb61a959096c84b567f215534faa78d0424/grpcio-1.83.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c", size = 7570491, upload-time = "2026-07-23T15:19:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/3e/15/108d30d5a5c964312ae8b9cb0e8cc5b3c1cc68d8f757cca52b3565534d26/grpcio-1.83.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5", size = 8605036, upload-time = "2026-07-23T15:19:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/ea/23/3828ae13c3db8233d123ad612747665817b952d8a954f32390230b582336/grpcio-1.83.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5", size = 7981587, upload-time = "2026-07-23T15:19:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/77af31228f55f55a2a5112bb0077ad0a1c4d23dbb0c2853a62475bbdcc14/grpcio-1.83.0-cp312-cp312-win32.whl", hash = "sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc", size = 4394004, upload-time = "2026-07-23T15:19:50.618Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/f706e39550e7a3732ce2b9c5926107a93d74a802775b19b642a6df27dc96/grpcio-1.83.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df", size = 5158525, upload-time = "2026-07-23T15:19:52.246Z" }, + { url = "https://files.pythonhosted.org/packages/56/eb/135daaa713f32d33b8f99b4153b3f8dc3b2a124996ac15581bf9ebdad3c3/grpcio-1.83.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0", size = 6304480, upload-time = "2026-07-23T15:19:53.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a1/121806ce69f23138dabe06aa595b0e5f1ae051a37e4c1954eed7d692c800/grpcio-1.83.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1", size = 12154419, upload-time = "2026-07-23T15:19:56.3Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e8/d0389e09cd6b4c4d3089b92967ae4e3ffd64795bd349bf2f85cd6656d3da/grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867", size = 6873200, upload-time = "2026-07-23T15:19:58.701Z" }, + { url = "https://files.pythonhosted.org/packages/f8/51/f464c1d211fa50d5adbabe1b2e519948d99c13757052bfc9ea7afa28e284/grpcio-1.83.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881", size = 7618811, upload-time = "2026-07-23T15:20:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c0/539fe0832f2dd6500a28f5263071623fb34e8d4867aec632ccf81bd21156/grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f", size = 7042310, upload-time = "2026-07-23T15:20:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ca/ccf617d37ffa72567fa8e005ec7090c99da922799be2fb9847c8b21ca18c/grpcio-1.83.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61", size = 7575412, upload-time = "2026-07-23T15:20:04.712Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b9/fd8d5245f823a8e0fd35d90e20ea3aa4acd47f8d5318fa8df307df52dec6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45", size = 8604248, upload-time = "2026-07-23T15:20:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/f37632fc11db72dfa4bba86c3a43e54358e53030df111ecae5e91a733ad6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c", size = 7977458, upload-time = "2026-07-23T15:20:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/93/b6/d70b69ae5c0cfc341b9ba474980e4ed99cbf05c0e4a14e9eee8cb73db0a5/grpcio-1.83.0-cp313-cp313-win32.whl", hash = "sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf", size = 4393993, upload-time = "2026-07-23T15:20:11.017Z" }, + { url = "https://files.pythonhosted.org/packages/0f/13/45d4cccb555cf4c476226979bf3d2fd0b0254216f7564c3a053e35117efc/grpcio-1.83.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735", size = 5159650, upload-time = "2026-07-23T15:20:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/9c/60/f2cca8147ea213d3e43ae9158d03ad04e020fdf32ff027253e1fe93f921d/grpcio-1.83.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa", size = 6305607, upload-time = "2026-07-23T15:20:15.353Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ab/d3874931d123a95e83a3ebf8aa04537988fb62425cedb8bf3cefc5ad41b2/grpcio-1.83.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c", size = 12166617, upload-time = "2026-07-23T15:20:17.435Z" }, + { url = "https://files.pythonhosted.org/packages/92/ff/6f18f9426b69306f4e00a9add3b0ee2748da8aad53836ef80cab0d62d04f/grpcio-1.83.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b", size = 6880213, upload-time = "2026-07-23T15:20:19.98Z" }, + { url = "https://files.pythonhosted.org/packages/70/21/706d1147c6b93b98f179240c13991fbcc56880eba0c868abb1ad40d8a0a6/grpcio-1.83.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c", size = 7618335, upload-time = "2026-07-23T15:20:22.161Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/1a8443c889115ec9e213a213e86bc93a71ee9088027e5befa09aaa0edd9d/grpcio-1.83.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df", size = 7043416, upload-time = "2026-07-23T15:20:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/c6/94e0fee5b12bc1da1370185b680988db6f739d19b42d9959db01a7ea50bf/grpcio-1.83.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b", size = 7583253, upload-time = "2026-07-23T15:20:26.313Z" }, + { url = "https://files.pythonhosted.org/packages/a0/97/de1ccb671fb85575bc5192faedf9ecdbdf5b390d2e6584dcf552bcbd370e/grpcio-1.83.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404", size = 8605102, upload-time = "2026-07-23T15:20:28.437Z" }, + { url = "https://files.pythonhosted.org/packages/17/0f/0e0ec749a7034ffcbaa050e39779872950ead90c22e7e0116be3f28b2b46/grpcio-1.83.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af", size = 7979826, upload-time = "2026-07-23T15:20:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/83/fa/c3fda157287f64bc65acee6c5aa90c41acf9e0d3a8e69a265eecff6d00a1/grpcio-1.83.0-cp314-cp314-win32.whl", hash = "sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33", size = 4471765, upload-time = "2026-07-23T15:20:33.195Z" }, + { url = "https://files.pythonhosted.org/packages/a1/00/b1b26431c9d54eee11724fd6e5585473a2ed47fbc1fb95e5204906a642ce/grpcio-1.83.0-cp314-cp314-win_amd64.whl", hash = "sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9", size = 5298932, upload-time = "2026-07-23T15:20:35.48Z" }, +] + +[[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 = "hf-xet" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/62/3c062f593bd92ef4e77a0ef39541e3d82a0a1d3947c8a777a02a13a27828/hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d", size = 4074584, upload-time = "2026-08-03T22:32:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1e/c0ad437dd267a8e435bef594acf781bbc3874ff0b6435b4962d03ecf7cc4/hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675", size = 3867381, upload-time = "2026-08-03T22:32:49.049Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ee/7c0d7b6ab336167531b1c30af2af003f054af4c749becbd7209ae33a77c3/hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b", size = 4453982, upload-time = "2026-08-03T22:32:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/ad8eab1c9525246650cbaa821caa3cdbaca734ab1a5b8c91bea09cbd8d69/hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522", size = 4249445, upload-time = "2026-08-03T22:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/d8/26/1eee8aedb0dafc1ab9717dc9ac602cde33361b232dc06803f1f6ed18b58c/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e", size = 4451099, upload-time = "2026-08-03T22:32:54.114Z" }, + { url = "https://files.pythonhosted.org/packages/67/57/0b88af1f194ab6c9c650547d9cc06bfeaab836ae4dcdb331676bfb8be95a/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9", size = 4664712, upload-time = "2026-08-03T22:32:55.547Z" }, + { url = "https://files.pythonhosted.org/packages/53/a0/26b717a9d1840e8abf48dcec64b5ed8fbe472671d38ad28d30e147132b33/hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338", size = 4025906, upload-time = "2026-08-03T22:32:57.391Z" }, + { url = "https://files.pythonhosted.org/packages/49/f6/4a9966633c6fef83af997e2cff68ec1963676d412bdfd096df2a93b8e185/hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765", size = 3849221, upload-time = "2026-08-03T22:32:59.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, +] + +[[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 = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b9/be66eb0decd730d89b9c94f930e4b8d87787b05724bb84af98bfd825f72c/httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826", size = 208805, upload-time = "2026-05-25T22:16:50.434Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f7/b4d41eaae2869d31356bc4bbf546f44fae83ff298af0a043ca0625b06773/httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77", size = 113527, upload-time = "2026-05-25T22:16:51.672Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e4/77487e14fc7be47180fd0eb4267c7486d0cc59b74031839a3daf8650136b/httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4", size = 450035, upload-time = "2026-05-25T22:16:53.313Z" }, + { url = "https://files.pythonhosted.org/packages/da/72/5a8f787e323f56fbd86c32a4be92a86776e4cfe8b4317db999f452028362/httptools-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb", size = 451101, upload-time = "2026-05-25T22:16:54.696Z" }, + { url = "https://files.pythonhosted.org/packages/ed/41/b44a25560955197674b6744cb903664300e239235a5eaa69df0890d87054/httptools-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813", size = 436140, upload-time = "2026-05-25T22:16:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/74/b0/054aac84c03d7e097bf4c605fb7e74eec3d65c0276adf64ee97f3a103ff5/httptools-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba", size = 437041, upload-time = "2026-05-25T22:16:57.716Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e8/86b85bbc0ac7892232f1a99ab96a9aa71936984fa06adfc0afc83ca7789e/httptools-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557", size = 90454, upload-time = "2026-05-25T22:16:58.871Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[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 = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "httpx2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ae/222a91937ebee7f62c0ca8f5ee0afd97577caf24c0abb927d1f5c7e9f6d2/huggingface_hub-1.28.0.tar.gz", hash = "sha256:46a2e950c09234de54093d587d1675382f0d08dbd600d9fb599b5932f5b2c6cb", size = 959609, upload-time = "2026-08-18T12:27:15.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/0e/eafef18f1a75e125e68395db21131db0cf868a128ecd2fce69b4df6c584b/huggingface_hub-1.28.0-py3-none-any.whl", hash = "sha256:58a8bacb03072edfc38067065e9dc24bbb34805410fcd36a1632de0b329660bb", size = 793202, upload-time = "2026-08-18T12:27:12.719Z" }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + +[[package]] +name = "importlib-resources" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "instructor" +version = "1.15.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "docstring-parser" }, + { name = "jinja2" }, + { name = "jiter" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/24/f6b28e83b3194c6223ed7c6eed5724687f6ecd378ec2ff24044f0cbf1f09/instructor-1.15.4.tar.gz", hash = "sha256:ea2280c3678d0f6891c4d826104f95624b680e69877113a6345b1d7c9027ba0f", size = 70049678, upload-time = "2026-06-28T07:36:43.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/8d/f668a30fff4d25b36533355e23aeb0b5724df4628eb974124ed64b7bcf8d/instructor-1.15.4-py3-none-any.whl", hash = "sha256:00e0ecda80fd9746fb6d082d3f9641e193adb1d8849f0775f91519a82aeff968", size = 252522, upload-time = "2026-06-28T07:36:36.863Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/2e/a9959997739c403378d0a4a3a1c4ed80b60aeace216c4d37b303a9fc60a4/jiter-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:02f36a5c700f105ac04a6556fe664a59037a2c200db3b7e88784fac2ddf02531", size = 316927, upload-time = "2026-04-10T14:25:40.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/72/b6de8a531e0adbadd839bec301165feb1fccf00e9ff55073ba2dd20f0043/jiter-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41eab6c09ceffb6f0fe25e214b3068146edb1eda3649ca2aee2a061029c7ba2e", size = 321181, upload-time = "2026-04-10T14:25:42.621Z" }, + { url = "https://files.pythonhosted.org/packages/db/d8/2040b9efa13c917f855c40890ae4119fe02c25b7c7677d5b4fa820a851fc/jiter-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf4d4c109641f9cfaf4a7b6aebd51654e405cd00fa9ebbf87163b8b97b325aa", size = 347387, upload-time = "2026-04-10T14:25:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/655c0ad5ce6a8e90f9068c175b8a236877d753e460762b3183c136db1c5b/jiter-0.14.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80c7b41a628e6be2213ad0ece763c5f88aa5ee003fa394d58acaaee1f4b8342", size = 373083, upload-time = "2026-04-10T14:25:45.55Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/549c40fa068f08710b7570869c306a051eb67a29758bd64f4114f730554c/jiter-0.14.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb3dbf7cc0d4dbe73cce307ebe7eefa7f73a7d3d854dd119ea0c243f03e40927", size = 463639, upload-time = "2026-04-10T14:25:47.452Z" }, + { url = "https://files.pythonhosted.org/packages/25/2f/97a32a05fed14ed58a18e181fdfb619e05163f3726b54ee6080ec0539c09/jiter-0.14.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7054adcdeb06b46efd17b5734f75817a44a2d06d3748e36c3a023a1bb52af9ec", size = 380735, upload-time = "2026-04-10T14:25:49.305Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3b/4347e1d6c2a973d653bbb7a2d671a2d2426e54b52ba735b8ff0d0a29b75c/jiter-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d597cd1bf6790376f3fffc7c708766e57301d99a19314824ea0ccc9c3c70e1e2", size = 358632, upload-time = "2026-04-10T14:25:50.931Z" }, + { url = "https://files.pythonhosted.org/packages/ef/24/ca452fbf2ea33548ed30ce68a39a50442d3f7c9bf0704a7af958a930c057/jiter-0.14.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:df63a14878da754427926281626fd3ee249424a186e25a274e78176d42945264", size = 359969, upload-time = "2026-04-10T14:25:52.381Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a3/94470a0d199287caabeb4da2bb2ae5f6d17f3cf05dfc975d7cb064d58e0f/jiter-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ea73187627bcc5810e085df715e8a99da8bdfd96a7eb36b4b4df700ba6d4c9c", size = 397529, upload-time = "2026-04-10T14:25:53.801Z" }, + { url = "https://files.pythonhosted.org/packages/cf/71/6768edc09d7c45c39f093feb3de105fa718a3e982b5208b8a2ed6382b44b/jiter-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9f541eaf7bb8382367a1a23d6fc3d6aad57f8dd8c18c3c17f838bee20f217220", size = 522342, upload-time = "2026-04-10T14:25:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6b/5c2e17559a0f4e96e934479f7137df46c939e983fa05244e674815befb73/jiter-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:107465250de4fce00fdb47166bcd51df8e634e049541174fe3c71848e44f52ce", size = 556784, upload-time = "2026-04-10T14:25:56.927Z" }, + { url = "https://files.pythonhosted.org/packages/b1/83/c25f3556a60fc74d11199100f1b6cc0c006b815c8494dea8ca16fe398732/jiter-0.14.0-cp310-cp310-win32.whl", hash = "sha256:ffb2a08a406465bb076b7cc1df41d833106d3cf7905076cc73f0cb90078c7d10", size = 208439, upload-time = "2026-04-10T14:25:58.796Z" }, + { url = "https://files.pythonhosted.org/packages/2e/99/781a1b413f0989b7f2ea203b094b331685f1a35e52e0a45e5d000ecaab27/jiter-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb8b682d10cb0cce7ff4c1af7244af7022c9b01ae16d46c357bdd0df13afb25d", size = 204558, upload-time = "2026-04-10T14:26:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, + { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, + { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, + { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, + { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, + { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, + { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "json-repair" +version = "0.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a6/d69888cb4ffde30e80db1e6c32caaadd2f984a80067d5ea72c2cb3f61c3f/json_repair-0.60.1.tar.gz", hash = "sha256:841661cdd2df507c9a4e189097f38ca6bc372e06d4b4e36d72e590f68176c290", size = 49451, upload-time = "2026-06-03T17:28:44.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/1f/2a2b5eea8ef5762a86ad3f8fddddaaba2c0d76dd44e644b9158900868bec/json_repair-0.60.1-py3-none-any.whl", hash = "sha256:ba6ff974f2a8bef2f7768144a7f03f870a816443f03da27a49cdd0ec31a78049", size = 48045, upload-time = "2026-06-03T17:28:43.038Z" }, +] + +[[package]] +name = "json5" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bbe62f3d0c05a689c711cff57b2e3ac3d3e526380adb7c781989f075115c/json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559", size = 48202, upload-time = "2024-11-26T19:56:37.823Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/42/797895b952b682c3dafe23b1834507ee7f02f4d6299b65aaa61425763278/json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa", size = 34049, upload-time = "2024-11-26T19:56:36.649Z" }, +] + +[[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 = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kubernetes" +version = "36.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "durationpy" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/57/b07b96353f902aa1bdbe00e878e3a12a137977d03a962479785576aa8ec9/kubernetes-36.0.3.tar.gz", hash = "sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7", size = 2337528, upload-time = "2026-07-13T20:38:12.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/30/a96d47df739689ac0001ade0afefc16e3b477fc2fb426b568515fdc8afce/kubernetes-36.0.3-py2.py3-none-any.whl", hash = "sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f", size = 4618066, upload-time = "2026-07-13T20:38:10.172Z" }, +] + +[[package]] +name = "lance-namespace" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lance-namespace-urllib3-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/93/da5f7fcac690db9b282a3439ed9e34960c147619a0d6e1f4eb8cd240e7a5/lance_namespace-0.11.1.tar.gz", hash = "sha256:f67cfbbe0647b7cb42f23b673e7edf8a75b7d8a047265a916492f8d247ee1bc2", size = 11631, upload-time = "2026-08-18T17:40:06.294Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/bc/601f2b3cc4cfa0070d858a33223bc823fffdd7981a25c45984a5216ca952/lance_namespace-0.11.1-py3-none-any.whl", hash = "sha256:07643fce9a42ad4d58cc8bf91e3f592bc7f4cbd8d0ad5233223506debf67551c", size = 13507, upload-time = "2026-08-18T17:40:03.561Z" }, +] + +[[package]] +name = "lance-namespace-urllib3-client" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/c5/2bdd0ff98b469894c8a73be809d26ffdad5402517b0e5f9e758026cba29e/lance_namespace_urllib3_client-0.11.1.tar.gz", hash = "sha256:145a9e9424d7597487249b5b95ee274423bf2910e1a9160b6a07b676b61ea46a", size = 237345, upload-time = "2026-08-18T17:40:07.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/eaaefd55d1190291207049fedc6b3eb22b506e57d6de91bae46bbaaa9c60/lance_namespace_urllib3_client-0.11.1-py3-none-any.whl", hash = "sha256:36537f529294da6d884ba0fe783704483f0a75463497c7705fd083a4d0257990", size = 406311, upload-time = "2026-08-18T17:40:04.842Z" }, +] + +[[package]] +name = "lancedb" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "lance-namespace" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/1577778ad57dba0c55dc13d87230583e14541c82562483ecf8bb2f8e8a00/lancedb-0.30.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:be2a9a43a65c330ccfd08115afb26106cd8d16788522fe7693d3a1f4e01ad321", size = 41959907, upload-time = "2026-03-16T23:03:04.551Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/8c2a04ce499a2a97d1a0de2b7e84fa8166f988a9a495e1ada860110489c2/lancedb-0.30.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6a4ba2a1799a426cbf2ba5ea2559a7389a569e9a31f2409d531ceb59d42f35", size = 43873070, upload-time = "2026-03-16T23:11:01.352Z" }, + { url = "https://files.pythonhosted.org/packages/16/68/e01bf7837454a5ce9e2f6773905e07b09a949bc88136c0773c8166ed7729/lancedb-0.30.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a967ec05f9930770aeb077bc5579769b1bedf559fcd03a592d9644084625918", size = 46891197, upload-time = "2026-03-16T23:14:39.18Z" }, + { url = "https://files.pythonhosted.org/packages/43/d1/9085ad17abd98f3a180d7860df3190b2d76f99f533c76d7c7494cec4139d/lancedb-0.30.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:05c66f40f7d4f6f24208e786c40f84b87b1b8e55505305849dd3fed3b78431a3", size = 43877660, upload-time = "2026-03-16T23:11:00.837Z" }, + { url = "https://files.pythonhosted.org/packages/ea/69/504ee25c57c3f23c80276b5b7b5e4c0f98a5197a7e9e51d3c50500d2b53a/lancedb-0.30.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:bdcd27d98554ed11b6f345b14d1307b0e2332d5654767e9ee2e23d9b2d6513d1", size = 46932144, upload-time = "2026-03-16T23:15:00.474Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/d5550f22023e672af1945394f7a06a578fcab2980ecc6666acef3428a771/lancedb-0.30.0-cp39-abi3-win_amd64.whl", hash = "sha256:4751ff0446b90be4d4dccfe05f6c105f403a05f3b8531ab99eedc1c656aca950", size = 51121310, upload-time = "2026-03-16T23:43:23.89Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.5.6" +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/27/b9/893806b89f77e1271fe6e10ce41682ff5fe43d071564e1d8e39dbb5d4d6d/langchain_core-1.5.6.tar.gz", hash = "sha256:b5f73bd9688c457b31ec73657a0ad56948f889fae27acee79286e9c285632ee6", size = 984873, upload-time = "2026-08-17T21:26:35.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/0a/890504397885c9d1ae45f2e06c3000dd9f4445439602b6a990a88b32c0ac/langchain_core-1.5.6-py3-none-any.whl", hash = "sha256:d6cf37bf695ecc22cddeb8461a684e353190b2ce430d99eb22bc11c0c7c00ea5", size = 567016, upload-time = "2026-08-17T21:26:34.595Z" }, +] + +[[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 = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "llama-index-core" +version = "0.14.23" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aiosqlite" }, + { name = "banks" }, + { name = "dataclasses-json" }, + { name = "deprecated" }, + { name = "dirtyjson" }, + { name = "filetype" }, + { name = "fsspec" }, + { name = "httpx" }, + { name = "llama-index-workflows" }, + { name = "nest-asyncio" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nltk" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pillow" }, + { name = "platformdirs" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "tenacity" }, + { name = "tiktoken" }, + { name = "tinytag" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "typing-inspect" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/ac/f885ae14317af43a026c909ea4d2083fcee2f0d014f90426b5b9aa1f9912/llama_index_core-0.14.23.tar.gz", hash = "sha256:c4baf2f2ab4f84e95090fe7941e0c87d6c514304f7bd2a749b8fa22164c1822b", size = 11588373, upload-time = "2026-06-24T19:35:55.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/d5/05d61f34c01c6578fb758d0a3ddef58d36c6ffa9a9f84a5c9a16262ad94d/llama_index_core-0.14.23-py3-none-any.whl", hash = "sha256:6a54d267826732a8507f81df40785b107f7592af20f451a39a59005147caf84c", size = 11924908, upload-time = "2026-06-24T19:35:52.833Z" }, +] + +[[package]] +name = "llama-index-instrumentation" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/d0/671b23ccff255c9bce132a84ffd5a6f4541ceefdeab9c1786b08c9722f2e/llama_index_instrumentation-0.5.0.tar.gz", hash = "sha256:eeb724648b25d149de882a5ac9e21c5acb1ce780da214bda2b075341af29ad8e", size = 43831, upload-time = "2026-03-12T20:17:06.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/45/6dcaccef44e541ffa138e4b45e33e0d40ab2a7d845338483954fcf77bc75/llama_index_instrumentation-0.5.0-py3-none-any.whl", hash = "sha256:aaab83cddd9dd434278891012d8995f47a3bc7ed1736a371db90965348c56a21", size = 16444, upload-time = "2026-03-12T20:17:05.957Z" }, +] + +[[package]] +name = "llama-index-workflows" +version = "2.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-instrumentation" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/2f/97339da1feaa09716287c8d30ee04b0e6dfa1ab2e2aa4ac2249e2011762d/llama_index_workflows-2.23.2.tar.gz", hash = "sha256:6d791acbe11947c7e9cb02c886356e29c2b4d66f97c6593fe7eb3d288e85b351", size = 137112, upload-time = "2026-08-17T16:38:16.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/05/bacf8b5ae50d035c3e27ea7cf83761f62705c490e6bbf37e60b8dd53a06e/llama_index_workflows-2.23.2-py3-none-any.whl", hash = "sha256:7dba6033fe776a25dcc74c163543a12a93d5f524cc7df33b193ea383b3577b6d", size = 165028, upload-time = "2026-08-17T16:38:15.005Z" }, +] + +[[package]] +name = "logfire-api" +version = "4.40.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/5f/f4d0fb5c29d876c533daf415c0d961e1c4d0284167ed0834644a28581230/logfire_api-4.40.0.tar.gz", hash = "sha256:f4631d5ca6af95e9d4dadc4f63619ebb8f2300eecfca0ca99c84403d6ea605de", size = 90781, upload-time = "2026-08-05T11:27:00.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/be/ebe35d94e7d567b58d79bd7e1085fe85195ad4b8c8df8882a5a46caa4984/logfire_api-4.40.0-py3-none-any.whl", hash = "sha256:f8b7309235a942368b927f00e0a1869ff0820833f264a30e77a35f1da829c130", size = 140593, upload-time = "2026-08-05T11:26:58.395Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "marshmallow" +version = "3.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, +] + +[[package]] +name = "mcp" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/bb/88ee54afa5644b0f35ab5b435f208394feb963e5bb47c4e404deb625ffa4/mmh3-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f", size = 56080, upload-time = "2026-03-05T15:53:40.452Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/5404c2fd6ac84819e8ff1b7e34437b37cf55a2b11318894909e7bb88de3f/mmh3-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb", size = 40462, upload-time = "2026-03-05T15:53:41.751Z" }, + { url = "https://files.pythonhosted.org/packages/de/0b/52bffad0b52ae4ea53e222b594bd38c08ecac1fc410323220a7202e43da5/mmh3-5.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c", size = 40077, upload-time = "2026-03-05T15:53:42.753Z" }, + { url = "https://files.pythonhosted.org/packages/a0/9e/326c93d425b9fa4cbcdc71bc32aaba520db37577d632a24d25d927594eca/mmh3-5.2.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045", size = 95302, upload-time = "2026-03-05T15:53:43.867Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b1/e20d5f0d19c4c0f3df213fa7dcfa0942c4fb127d38e11f398ae8ddf6cccc/mmh3-5.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f", size = 101174, upload-time = "2026-03-05T15:53:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4a/1a9bb3e33c18b1e1cee2c249a3053c4d4d9c93ecb30738f39a62249a7e86/mmh3-5.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386", size = 103979, upload-time = "2026-03-05T15:53:46.334Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/dab9ee7545429e7acdd38d23d0104471d31de09a0c695f1b751e0ff34532/mmh3-5.2.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a", size = 110898, upload-time = "2026-03-05T15:53:47.443Z" }, + { url = "https://files.pythonhosted.org/packages/72/08/408f11af7fe9e76b883142bb06536007cc7f237be2a5e9ad4e837716e627/mmh3-5.2.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0", size = 118308, upload-time = "2026-03-05T15:53:49.1Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/0551be7fe0000736d9ad12ffa1f130d7a0c17b49193d6dc41c82bd9404c6/mmh3-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb", size = 101671, upload-time = "2026-03-05T15:53:50.317Z" }, + { url = "https://files.pythonhosted.org/packages/44/17/6e4f80c4e6ad590139fa2017c3aeca54e7cc9ef68e08aa142a0c90f40a97/mmh3-5.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890", size = 96682, upload-time = "2026-03-05T15:53:51.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a7/b82fccd38c1fa815de72e94ebe9874562964a10e21e6c1bc3b01d3f15a0e/mmh3-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a", size = 110287, upload-time = "2026-03-05T15:53:52.68Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a1/2644069031c8cec0be46f0346f568a53f42fddd843f03cc890306699c1e2/mmh3-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5", size = 111899, upload-time = "2026-03-05T15:53:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/51/7b/6614f3eb8fb33f931fa7616c6d477247e48ec6c5082b02eeeee998cffa94/mmh3-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57", size = 100078, upload-time = "2026-03-05T15:53:55.234Z" }, + { url = "https://files.pythonhosted.org/packages/27/9a/dd4d5a5fb893e64f71b42b69ecae97dd78db35075412488b24036bc5599c/mmh3-5.2.1-cp310-cp310-win32.whl", hash = "sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518", size = 40756, upload-time = "2026-03-05T15:53:56.319Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/0b25889450f8aeffcec840aa73251e853f059c1b72ed1d1c027b956f95f5/mmh3-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f", size = 41519, upload-time = "2026-03-05T15:53:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/8fd42e3c526d0bcb1db7f569c0de6729e180860a0495e387a53af33c2043/mmh3-5.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0", size = 39285, upload-time = "2026-03-05T15:53:58.697Z" }, + { url = "https://files.pythonhosted.org/packages/65/d7/3312a59df3c1cdd783f4cf0c4ee8e9decff9c5466937182e4cc7dbbfe6c5/mmh3-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450", size = 56082, upload-time = "2026-03-05T15:53:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/61/96/6f617baa098ca0d2989bfec6d28b5719532cd8d8848782662f5b755f657f/mmh3-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0", size = 40458, upload-time = "2026-03-05T15:54:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b4/9cd284bd6062d711e13d26c04d4778ab3f690c1c38a4563e3c767ec8802e/mmh3-5.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082", size = 40079, upload-time = "2026-03-05T15:54:02.743Z" }, + { url = "https://files.pythonhosted.org/packages/f6/09/a806334ce1d3d50bf782b95fcee8b3648e1e170327d4bb7b4bad2ad7d956/mmh3-5.2.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997", size = 97242, upload-time = "2026-03-05T15:54:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/ee/93/723e317dd9e041c4dc4566a2eb53b01ad94de31750e0b834f1643905e97c/mmh3-5.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d", size = 103082, upload-time = "2026-03-05T15:54:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/f96121e69cc48696075071531cf574f112e1ffd08059f4bffb41210e6fc5/mmh3-5.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e", size = 106054, upload-time = "2026-03-05T15:54:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/192b987ec48d0b2aecf8ac285a9b11fbc00030f6b9c694664ae923458dde/mmh3-5.2.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d", size = 112910, upload-time = "2026-03-05T15:54:09.403Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/03e91fd334ed0144b83343a76eb11f17434cd08f746401488cfeafb2d241/mmh3-5.2.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4", size = 120551, upload-time = "2026-03-05T15:54:10.587Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/b89a71d2ff35c3a764d1c066c7313fc62c7cc48fa48a4b3b0304a4a0146f/mmh3-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15", size = 99096, upload-time = "2026-03-05T15:54:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/36/b5/613772c1c6ed5f7b63df55eb131e887cc43720fec392777b95a79d34e640/mmh3-5.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503", size = 98524, upload-time = "2026-03-05T15:54:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/1524566fe8eaf871e4f7bc44095929fcd2620488f402822d848df19d679c/mmh3-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2", size = 106239, upload-time = "2026-03-05T15:54:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/04/94/21adfa7d90a7a697137ad6de33eeff6445420ca55e433a5d4919c79bc3b5/mmh3-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1", size = 109797, upload-time = "2026-03-05T15:54:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e6/1aacc3a219e1aa62fa65669995d4a3562b35be5200ec03680c7e4bec9676/mmh3-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38", size = 97228, upload-time = "2026-03-05T15:54:16.992Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b9/5e4cca8dcccf298add0a27f3c357bc8cf8baf821d35cdc6165e4bd5a48b0/mmh3-5.2.1-cp311-cp311-win32.whl", hash = "sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728", size = 40751, upload-time = "2026-03-05T15:54:18.714Z" }, + { url = "https://files.pythonhosted.org/packages/72/fc/5b11d49247f499bcda591171e9cf3b6ee422b19e70aa2cef2e0ae65ca3b9/mmh3-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a", size = 41517, upload-time = "2026-03-05T15:54:19.764Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/2a511ee8a1c2a527c77726d5231685b72312c5a1a1b7639ad66a9652aa84/mmh3-5.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f", size = 39287, upload-time = "2026-03-05T15:54:20.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, + { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, + { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, + { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, + { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330, upload-time = "2026-03-05T15:54:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078, upload-time = "2026-03-05T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498, upload-time = "2026-03-05T15:54:49.942Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320, upload-time = "2026-03-05T15:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" }, + { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686, upload-time = "2026-03-05T15:55:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769, upload-time = "2026-03-05T15:55:05.277Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563, upload-time = "2026-03-05T15:55:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310, upload-time = "2026-03-05T15:55:07.323Z" }, + { url = "https://files.pythonhosted.org/packages/63/b4/65bc1fb2bb7f83e91c30865023b1847cf89a5f237165575e8c83aa536584/mmh3-5.2.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227", size = 40794, upload-time = "2026-03-05T15:55:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/c4/86/7168b3d83be8eb553897b1fac9da8bbb06568e5cfe555ffc329ebb46f59d/mmh3-5.2.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0", size = 41923, upload-time = "2026-03-05T15:55:10.924Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9b/b653ab611c9060ce8ff0ba25c0226757755725e789292f3ca138a58082cd/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b", size = 39131, upload-time = "2026-03-05T15:55:11.961Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5a2e0d34ab4d33543f01121e832395ea510132ea8e52cdf63926d9d81754/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966", size = 39825, upload-time = "2026-03-05T15:55:13.013Z" }, + { url = "https://files.pythonhosted.org/packages/bd/69/81699a8f39a3f8d368bec6443435c0c392df0d200ad915bf0d222b588e03/mmh3-5.2.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b", size = 40344, upload-time = "2026-03-05T15:55:14.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/71c8c775807606e8fd8acc5c69016e1caf3200d50b50b6dd4b40ce10b76c/mmh3-5.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8", size = 56291, upload-time = "2026-03-05T15:55:15.137Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7", size = 40575, upload-time = "2026-03-05T15:55:16.518Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e", size = 40052, upload-time = "2026-03-05T15:55:17.735Z" }, + { url = "https://files.pythonhosted.org/packages/97/ca/120d92223a7546131bbbc31c9174168ee7a73b1366f5463ffe69d9e691fe/mmh3-5.2.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74", size = 97311, upload-time = "2026-03-05T15:55:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc", size = 103279, upload-time = "2026-03-05T15:55:20.448Z" }, + { url = "https://files.pythonhosted.org/packages/48/29/ad97f4be1509cdcb28ae32c15593ce7c415db47ace37f8fad35b493faa9a/mmh3-5.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617", size = 106290, upload-time = "2026-03-05T15:55:21.6Z" }, + { url = "https://files.pythonhosted.org/packages/77/29/1f86d22e281bd8827ba373600a4a8b0c0eae5ca6aa55b9a8c26d2a34decc/mmh3-5.2.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2", size = 113116, upload-time = "2026-03-05T15:55:22.826Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/339971ea7ed4c12d98f421f13db3ea576a9114082ccb59d2d1a0f00ccac1/mmh3-5.2.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312", size = 120740, upload-time = "2026-03-05T15:55:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/3c7c4bdb8e926bb3c972d1e2907d77960c1c4b250b41e8366cf20c6e4373/mmh3-5.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb", size = 99143, upload-time = "2026-03-05T15:55:25.456Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/33dd8706e732458c8375eae63c981292de07a406bad4ec03e5269654aa2c/mmh3-5.2.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a", size = 98703, upload-time = "2026-03-05T15:55:26.723Z" }, + { url = "https://files.pythonhosted.org/packages/51/04/76bbce05df76cbc3d396f13b2ea5b1578ef02b6a5187e132c6c33f99d596/mmh3-5.2.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105", size = 106484, upload-time = "2026-03-05T15:55:28.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8f/c6e204a2c70b719c1f62ffd9da27aef2dddcba875ea9c31ca0e87b975a46/mmh3-5.2.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a", size = 110012, upload-time = "2026-03-05T15:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/7181efd8e39db386c1ebc3e6b7d1f702a09d7c1197a6f2742ed6b5c16597/mmh3-5.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd", size = 97508, upload-time = "2026-03-05T15:55:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/afa7ca2615fd85e1469474bb860e381443d0b868c083b62b41cb1d7ca32f/mmh3-5.2.1-cp314-cp314-win32.whl", hash = "sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4", size = 41387, upload-time = "2026-03-05T15:55:32.403Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/46d42a260ee1357db3d486e6c7a692e303c017968e14865e00efa10d09fc/mmh3-5.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb", size = 42101, upload-time = "2026-03-05T15:55:33.646Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7b/848a8378059d96501a41159fca90d6a99e89736b0afbe8e8edffeac8c74b/mmh3-5.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe", size = 39836, upload-time = "2026-03-05T15:55:35.026Z" }, + { url = "https://files.pythonhosted.org/packages/27/61/1dabea76c011ba8547c25d30c91c0ec22544487a8750997a27a0c9e1180b/mmh3-5.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba", size = 57727, upload-time = "2026-03-05T15:55:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/731185950d1cf2d5e28979cc8593016ba1619a295faba10dda664a4931b5/mmh3-5.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00", size = 41308, upload-time = "2026-03-05T15:55:37.254Z" }, + { url = "https://files.pythonhosted.org/packages/76/aa/66c76801c24b8c9418b4edde9b5e57c75e72c94e29c48f707e3962534f18/mmh3-5.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8", size = 40758, upload-time = "2026-03-05T15:55:38.61Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bb/79a1f638a02f0ae389f706d13891e2fbf7d8c0a22ecde67ba828951bb60a/mmh3-5.2.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc", size = 109670, upload-time = "2026-03-05T15:55:40.13Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/8cd0e187a288985bcfc79bf5144d1d712df9dee74365f59d26e3a1865be6/mmh3-5.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f", size = 117399, upload-time = "2026-03-05T15:55:42.076Z" }, + { url = "https://files.pythonhosted.org/packages/42/94/dfea6059bd5c5beda565f58a4096e43f4858fb6d2862806b8bbd12cbb284/mmh3-5.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44", size = 120386, upload-time = "2026-03-05T15:55:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/47/cb/f9c45e62aaa67220179f487772461d891bb582bb2f9783c944832c60efd9/mmh3-5.2.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7", size = 125924, upload-time = "2026-03-05T15:55:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/fe54a4a7c11bc9f623dfc1707decd034245602b076dfc1dcc771a4163170/mmh3-5.2.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c", size = 135280, upload-time = "2026-03-05T15:55:45.866Z" }, + { url = "https://files.pythonhosted.org/packages/97/67/fe7e9e9c143daddd210cd22aef89cbc425d58ecf238d2b7d9eb0da974105/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac", size = 110050, upload-time = "2026-03-05T15:55:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/6d4b09fcbef80794de447c9378e39eefc047156b290fa3dd2d5257ca8227/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912", size = 111158, upload-time = "2026-03-05T15:55:48.239Z" }, + { url = "https://files.pythonhosted.org/packages/81/a6/ca51c864bdb30524beb055a6d8826db3906af0834ec8c41d097a6e8573d5/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf", size = 116890, upload-time = "2026-03-05T15:55:49.405Z" }, + { url = "https://files.pythonhosted.org/packages/cc/04/5a1fe2e2ad843d03e89af25238cbc4f6840a8bb6c4329a98ab694c71deda/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d", size = 123121, upload-time = "2026-03-05T15:55:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/af/4d/3c820c6f4897afd25905270a9f2330a23f77a207ea7356f7aadace7273c0/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18", size = 110187, upload-time = "2026-03-05T15:55:52.143Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/1d71cd143752361c0aebef16ad3f55926a6faf7b112d355745c1f8a25f7f/mmh3-5.2.1-cp314-cp314t-win32.whl", hash = "sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82", size = 41934, upload-time = "2026-03-05T15:55:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/63a2a88f31d93dea03947cccc2a076946857e799ea4f7acdecbf43b324aa/mmh3-5.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb", size = 43036, upload-time = "2026-03-05T15:55:55.252Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391, upload-time = "2026-03-05T15:55:56.697Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nltk" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "defusedxml" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/e6/fe51d2bb1a3b446f59c5c8165999a9fee208bc346af90a7cbf7657bc0d75/nltk-3.10.3.tar.gz", hash = "sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4", size = 5137152, upload-time = "2026-08-12T23:46:37.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/db/db/81bf3d7cecfbfed9092b6b4052e857a769d62ed90561b410014e0aae18db/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36", size = 19153079, upload-time = "2025-10-27T23:05:57.686Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/a382452b17cf70a2313153c520ea4c96ab670c996cb3a95cc5d5ac7bfdac/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2", size = 15219883, upload-time = "2025-10-22T03:46:21.66Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/179bf90679984c85b417664c26aae4f427cba7514bd2d65c43b181b7b08b/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7", size = 17370357, upload-time = "2025-10-22T03:46:57.968Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6d/738e50c47c2fd285b1e6c8083f15dac1a5f6199213378a5f14092497296d/onnxruntime-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc", size = 13467651, upload-time = "2025-10-27T23:06:11.904Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/3c23a8f75f93122d2b3410bfb74d06d0f8da4ac663185f91866b03f7da1b/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", size = 19153857, upload-time = "2025-10-22T03:46:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/506eed9af03d86f8db4880a4c47cd0dffee973ef7e4f4cff9f1d4bcf7d22/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", size = 15220095, upload-time = "2025-10-22T03:46:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/e9/80/113381ba832d5e777accedc6cb41d10f9eca82321ae31ebb6bcede530cea/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", size = 17372080, upload-time = "2025-10-22T03:47:00.265Z" }, + { url = "https://files.pythonhosted.org/packages/3a/db/1b4a62e23183a0c3fe441782462c0ede9a2a65c6bbffb9582fab7c7a0d38/onnxruntime-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", size = 13468349, upload-time = "2025-10-22T03:47:25.783Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" }, + { url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" }, + { url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" }, + { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" }, + { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" }, +] + +[[package]] +name = "openai" +version = "2.54.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/9a/8c75e8c8a5b407a0586faeb2afac91674ff955c191ecc1d6d3b6669f6788/openai-2.54.0.tar.gz", hash = "sha256:e3e6f8bc1ba30ddf381ace1a14340eed381cb984a1a59bd0f34b5be3b5d49cfa", size = 1100285, upload-time = "2026-08-11T18:46:59.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/a8/bb76c7356de8ad57f59d5ff993d434df0607f07f08bcc9c9a5c275e399c0/openai-2.54.0-py3-none-any.whl", hash = "sha256:89089789197ccdb87f173a03145ed1598d00795220c93e96cf712b1cbf5e5f2b", size = 1660351, upload-time = "2026-08-11T18:46:56.684Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.63b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, +] + +[[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 = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pdfminer-six" +version = "20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" }, +] + +[[package]] +name = "pdfplumber" +version = "0.11.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pdfminer-six" }, + { name = "pillow" }, + { name = "pypdfium2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/56/6f450312ba05a27d7713b73857c1a25100dbda04fbc1331b13fb227a607d/pdfplumber-0.11.10.tar.gz", hash = "sha256:b95b2d28c66efb0a794a83b88c6c6aea5987532a445d20a1cbcfa657022e6e57", size = 102892, upload-time = "2026-06-15T03:31:31.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/9a/07d658e1e7fad860f1c541ab941348125dbdab773be3a0afaf32361866c7/pdfplumber-0.11.10-py3-none-any.whl", hash = "sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580", size = 60047, upload-time = "2026-06-15T03:31:29.702Z" }, +] + +[[package]] +name = "pendulum" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/72/9a51afa0a822b09e286c4cb827ed7b00bc818dac7bd11a5f161e493a217d/pendulum-3.2.0.tar.gz", hash = "sha256:e80feda2d10fa3ff8b1526715f7d33dcb7e08494b3088f2c8a3ac92d4a4331ce", size = 86912, upload-time = "2026-01-30T11:22:24.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/a4/934d8c97851bda5a034b0fd0512689173c8ca8cb3b87ebf8e5c1364d57f3/pendulum-3.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4a6bf778c6b42830b001c714dae5b9dad78da38e2e08203a4b0f5d53f8fa5e63", size = 338065, upload-time = "2026-01-30T11:20:36.467Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/2091275a9025f9b9ef9bf72ae386786a9b03af9515f5e2f5befb012ec91f/pendulum-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:625209bb7133d990905e8935e1c04f0a82315ae777b67910969b16f665d62c0b", size = 327426, upload-time = "2026-01-30T11:20:38.506Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/efc999e5b441a470df28964531c3ee7fce90dd2c510969132bba5897084e/pendulum-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f1d8641e8bd48b9b6f77f96fd498d3ecec63611ba8e7207e63936307846042", size = 340362, upload-time = "2026-01-30T11:20:40.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/71/bc88d786f0a10fcfdc5a0bac75c6cdb38df13ee09bc04d2e6ac0d3fd7948/pendulum-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a8d4212b1577ee3a034d18b360a9afa55bfc72789aeb805353be8b2ac132035", size = 373937, upload-time = "2026-01-30T11:20:42.242Z" }, + { url = "https://files.pythonhosted.org/packages/86/fb/48262b5b31fdfd68221cb92ab228657d0cd628fb35eca1a6f3aedad5ea09/pendulum-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e398d9e3db42d17f0c2cd39663c1c873ea6f11763ed6d126e2dcc92fc340d0dc", size = 379391, upload-time = "2026-01-30T11:20:43.736Z" }, + { url = "https://files.pythonhosted.org/packages/ae/72/cecb1710c36c6fe61e545050607c2050a2af0b991cf1a3d83981dfd895e8/pendulum-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04310463879a8d84534756ef9820d433e88b879203b6e10a5b416899dc05e7f1", size = 348433, upload-time = "2026-01-30T11:20:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/ec00008ba2f3298047a32b53588550a7ead84c579e7d7e4396474ab2f1ef/pendulum-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5b4f7491951c11bbdb20893817352c9140d31d1ae333839c34c0bca081a50a86", size = 517623, upload-time = "2026-01-30T11:20:46.741Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/541730ac4679e7f7ff5786aed21865c4f4a7d9b1d2693cfdbb891bdd5a5a/pendulum-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ffc169ad7595228d4dfc44d4e016846ff1bb5873b9f7ec70b0b1b51da0c77b3f", size = 561237, upload-time = "2026-01-30T11:20:48.252Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c1/165f10f2e37978caf92a1dca71726e7cd5d8de4039f9f4a6d1994a9b8d7f/pendulum-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:446f63d84ef21281844ceb45141536d3aabe291a821b6505e21a0d0e3ea95d67", size = 260733, upload-time = "2026-01-30T11:20:50.249Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/a4be6ec12161b503dd036f8d7cc57f8626170ae31bb298038be9af0001ce/pendulum-3.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5d775cc608c909ad415c8e789c84a9f120bb6a794c4215b2d8d910893cf0ec6a", size = 337923, upload-time = "2026-01-30T11:20:51.61Z" }, + { url = "https://files.pythonhosted.org/packages/59/e1/2a214e18355ec2a6ce3f683a97eecdb6050866ff3a6cf165d411450aeb1b/pendulum-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8de794a7f665aebc8c1ba4dd4b05ab8fe1a36ce9c0498366adf1d1edd79b2686", size = 327379, upload-time = "2026-01-30T11:20:53.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/01/7392e58ebc1d9e70b987dc8bb0c89710b47ac8125067efe7aa4c420b616f/pendulum-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bac7df7696e1c942e17c0556b3a7bcdd1d7aa5b24faee7620cb071e754a0622", size = 340115, upload-time = "2026-01-30T11:20:54.635Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/80de84c5ca1a3e4f7f3b75090c9b61b6dbb6d095e302ee592cebbaf0bbfb/pendulum-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db0f6a8a04475d9cba26ce701e7d66d266fd97227f2f5f499270eba04be1c7e9", size = 373969, upload-time = "2026-01-30T11:20:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/f7b4c1818927ab394a2a0a9b7011f360a0a75839a22678833c5bc0a84183/pendulum-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c352c63c1ff05f2198409b28498d7158547a8be23e1fbd4aa2cf5402fb239b55", size = 379058, upload-time = "2026-01-30T11:20:57.618Z" }, + { url = "https://files.pythonhosted.org/packages/36/94/9947cf710620afcc68751683f2f8de88d902505e7c13c0349d7e9d362f97/pendulum-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de8c1ad1d1aa7d4ceae341528bab35a0f8c88a5aa63f2f5d84e16b517d1b32c2", size = 348403, upload-time = "2026-01-30T11:20:59.56Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/0e6ba0bb00fa57907af2a3fca8643bded5dba1e87072d50673776a0d6ed2/pendulum-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1ba955511c12fec2252038b0c866c25c0c30b720bf74d3023710f121e42b1498", size = 517457, upload-time = "2026-01-30T11:21:01.602Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fe/dae5fbfe67bd41d943def0ad8f1e7f6988aa8e527255e433cd7c494f9ad5/pendulum-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4115bf364a2ec6d5ddc476751ceaa4164a04f2c15589f0d29aa210ddb784b15d", size = 561103, upload-time = "2026-01-30T11:21:03.924Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a0/8f646160b98abfc19152505af19bd643a4279ec2bdbe0959f16b7025fc6b/pendulum-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:4151a903356413fdd9549de0997b708fb95a214ed97803ffb479ffd834088378", size = 260595, upload-time = "2026-01-30T11:21:05.495Z" }, + { url = "https://files.pythonhosted.org/packages/79/01/feead7af9ded7a13f2d798fb6573e70f469113eafcd8cc8f59671584ca3e/pendulum-3.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:acfdee9ddc56053cb7c8c075afbfde0857322d09e56a56195b9cd127fae87e4c", size = 255382, upload-time = "2026-01-30T11:21:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/41/56/dd0ea9f97d25a0763cda09e2217563b45714786118d8c68b0b745395d6eb/pendulum-3.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bf0b489def51202a39a2a665dcc4162d5e46934a740fe4c4fe3068979610156c", size = 337830, upload-time = "2026-01-30T11:21:08.298Z" }, + { url = "https://files.pythonhosted.org/packages/cf/98/83d62899bf7226fc12396de4bc1fb2b5da27e451c7c60790043aaf8b4731/pendulum-3.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:937a529aa302efa18dcf25e53834964a87ffb2df8f80e3669ab7757a6126beaf", size = 327574, upload-time = "2026-01-30T11:21:09.715Z" }, + { url = "https://files.pythonhosted.org/packages/76/fa/ff2aa992b23f0543c709b1a3f3f9ed760ec71fd02c8bb01f93bf008b52e4/pendulum-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85c7689defc65c4dc29bf257f7cca55d210fabb455de9476e1748d2ab2ae80d7", size = 339891, upload-time = "2026-01-30T11:21:11.089Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4e/25b4fa11d19503d50d7b52d7ef943c0f20fd54422aaeb9e38f588c815c50/pendulum-3.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e216e5a412563ea2ecf5de467dcf3d02717947fcdabe6811d5ee360726b02b", size = 373726, upload-time = "2026-01-30T11:21:12.493Z" }, + { url = "https://files.pythonhosted.org/packages/4f/30/0acad6396c4e74e5c689aa4f0b0c49e2ecdcfce368e7b5bf35ca1c0fc61a/pendulum-3.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a2af22eeec438fbaac72bb7fba783e0950a514fba980d9a32db394b51afccec", size = 379827, upload-time = "2026-01-30T11:21:14.08Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f7/e6a2fdf2a23d59b4b48b8fa89e8d4bf2dd371aea2c6ba8fcecec20a4acb9/pendulum-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3159cceb54f5aa8b85b141c7f0ce3fac8bdd1ffdc7c79e67dca9133eac7c4d11", size = 348921, upload-time = "2026-01-30T11:21:15.816Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f2/c15fa7f9ad4e181aa469b6040b574988bd108ccdf4ae509ad224f9e4db44/pendulum-3.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c39ea5e9ffa20ea8bae986d00e0908bd537c8468b71d6b6503ab0b4c3d76e0ea", size = 517188, upload-time = "2026-01-30T11:21:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/47/c7/5f80b12ee88ec26e930c3a5a602608a63c29cf60c81a0eb066d583772550/pendulum-3.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e5afc753e570cce1f44197676371f68953f7d4f022303d141bb09f804d5fe6d7", size = 561833, upload-time = "2026-01-30T11:21:19.232Z" }, + { url = "https://files.pythonhosted.org/packages/90/15/1ac481626cb63db751f6281e294661947c1f0321ebe5d1c532a3b51a8006/pendulum-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:fd55c12560816d9122ca2142d9e428f32c0c083bf77719320b1767539c7a3a3b", size = 258725, upload-time = "2026-01-30T11:21:20.558Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/50b0398d7d027eb70a3e1e336de7b6e599c6b74431cb7d3863287e1292bb/pendulum-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:faef52a7ed99729f0838353b956f3fabf6c550c062db247e9e2fc2b48fcb9457", size = 253089, upload-time = "2026-01-30T11:21:22.497Z" }, + { url = "https://files.pythonhosted.org/packages/27/8c/400c8b8dbd7524424f3d9902ded64741e82e5e321d1aabbd68ade89e71cf/pendulum-3.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:addb0512f919fe5b70c8ee534ee71c775630d3efe567ea5763d92acff857cfc3", size = 337820, upload-time = "2026-01-30T11:21:24.305Z" }, + { url = "https://files.pythonhosted.org/packages/59/38/7c16f26cc55d9206d71da294ce6857d0da381e26bc9e0c2a069424c2b173/pendulum-3.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3aaa50342dc174acebdc21089315012e63789353957b39ac83cac9f9fc8d1075", size = 327551, upload-time = "2026-01-30T11:21:25.747Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cd/f36ec5d56d55104232380fdbf84ff53cc05607574af3cbdc8a43991ac8a7/pendulum-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:927e9c9ab52ff68e71b76dd410e5f1cd78f5ea6e7f0a9f5eb549aea16a4d5354", size = 339894, upload-time = "2026-01-30T11:21:27.229Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/b9a1e546519c3a92d5bc17787cea925e06a20def2ae344fa136d2fc40338/pendulum-3.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:249d18f5543c9f43aba3bd77b34864ec8cf6f64edbead405f442e23c94fce63d", size = 373766, upload-time = "2026-01-30T11:21:28.642Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a6/6471ab87ae2260594501f071586a765fc894817043b7d2d4b04e2eff4f31/pendulum-3.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c644cc15eec5fb02291f0f193195156780fd5a0affd7a349592403826d1a35e", size = 379837, upload-time = "2026-01-30T11:21:30.637Z" }, + { url = "https://files.pythonhosted.org/packages/0d/79/0ba0c14e862388f7b822626e6e989163c23bebe7f96de5ec4b207cbe7c3d/pendulum-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:063ab61af953bb56ad5bc8e131fd0431c915ed766d90ccecd7549c8090b51004", size = 348904, upload-time = "2026-01-30T11:21:32.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/34/df922c7c0b12719589d4954bfa5bdca9e02bcde220f5c5c1838a87118960/pendulum-3.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:26a3ae26c9dd70a4256f1c2f51addc43641813574c0db6ce5664f9861cd93621", size = 517173, upload-time = "2026-01-30T11:21:34.428Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/3b9e061eeee97b72a47c1434ee03f6d85f0284d9285d92b12b0fff2d19ac/pendulum-3.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:2b10d91dc00f424444a42f47c69e6b3bfd79376f330179dc06bc342184b35f9a", size = 561744, upload-time = "2026-01-30T11:21:35.861Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7e/f12fdb6070b7975c1fcfa5685dbe4ab73c788878a71f4d1d7e3c87979e37/pendulum-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:63070ff03e30a57b16c8e793ee27da8dac4123c1d6e0cf74c460ce9ee8a64aa4", size = 258746, upload-time = "2026-01-30T11:21:37.782Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/5abd872056357f069ae34a9b24a75ac58e79092d16201d779a8dd31386bb/pendulum-3.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8dde63e2796b62070a49ce813ce200aba9186130307f04ec78affcf6c2e8122", size = 253028, upload-time = "2026-01-30T11:21:39.381Z" }, + { url = "https://files.pythonhosted.org/packages/82/99/5b9cc823862450910bcb2c7cdc6884c0939b268639146d30e4a4f55eb1f1/pendulum-3.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c17ac069e88c5a1e930a5ae0ef17357a14b9cc5a28abadda74eaa8106d241c8e", size = 338281, upload-time = "2026-01-30T11:21:40.812Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3a/64a35260f6ac36c0ad50eeb5f1a465b98b0d7603f79a5c2077c41326d639/pendulum-3.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e1fbb540edecb21f8244aebfb05a1f2333ddc6c7819378c099d4a61cc91ae93c", size = 328030, upload-time = "2026-01-30T11:21:42.778Z" }, + { url = "https://files.pythonhosted.org/packages/da/6b/1140e09310035a2afb05bb90a2b8fbda9d3222e03b92de9533123afe6b65/pendulum-3.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8c67fb9a1fe8fc1adae2cc01b0c292b268c12475b4609ff4aed71c9dd367b4d", size = 340206, upload-time = "2026-01-30T11:21:44.148Z" }, + { url = "https://files.pythonhosted.org/packages/52/4a/a493de56cbc24a64b21ac6ba98513a9ec5c67daa3dba325e39a8e53f30d8/pendulum-3.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:baa9a66c980defda6cfe1275103a94b22e90d83ebd7a84cc961cee6cbd25a244", size = 373976, upload-time = "2026-01-30T11:21:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4c/f083c4fd1a161d4ab218680cc906338c541497b3098373f2241f58c429cb/pendulum-3.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef8f783fa7a14973b0596d8af2a5b2d90858a55030e9b4c6885eb4284b88314f", size = 380075, upload-time = "2026-01-30T11:21:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/57/b6/333a0fcb33bf15eb879a46a11ce6300c1698a141e689665fe430783ff8d6/pendulum-3.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d2e9bfb065727d8676e7ada3793b47a24349500a5e9637404355e482c822be", size = 349026, upload-time = "2026-01-30T11:21:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/43/1a/dfb526ec0cba1e7cd6a5e4f4dd64a6ada7428d1449c54b15f7b295f6e122/pendulum-3.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:55d7ba6bb74171c3ee409bf30076ee3a259a3c2bb147ac87ebb76aaa3cf5d3a2", size = 517395, upload-time = "2026-01-30T11:21:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/c9/37/b4f2b5f1200351c4869b8b46ad5c21019e3dbe0417f5867ae969fad7b5fe/pendulum-3.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:a50d8cf42f06d3d8c3f8bb2a7ac47fa93b5145e69de6a7209be6a47afdd9cf76", size = 561926, upload-time = "2026-01-30T11:21:51.698Z" }, + { url = "https://files.pythonhosted.org/packages/a0/9e/567376582da58f5fe8e4f579db2bcfbf243cf619a5825bdf1023ad1436b3/pendulum-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e5bbb92b155cd5018b3cf70ee49ed3b9c94398caaaa7ed97fe41e5bb5a968418", size = 258817, upload-time = "2026-01-30T11:21:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/95/67/dfffd7eb50d67fa821cd4d92cf71575ead6162930202bc40dfcedf78c38c/pendulum-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:d53134418e04335c3029a32e9341cccc9b085a28744fb5ee4e6a8f5039363b1a", size = 253292, upload-time = "2026-01-30T11:21:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0d/d5ac8468a1b40f09a62d6e91654088de432367907579dd161c0fb1bdf222/pendulum-3.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9585594d32faa71efa5a78f576f1ee4f79e9c5340d7c6f0cd6c5dfe725effaaa", size = 338760, upload-time = "2026-01-30T11:22:12.225Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/7fa8c8be6caac8e0be78fbe7668df571f44820ed779cb3736fab645fcba8/pendulum-3.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:26401e2de77c437e8f3b6160c08c6c5d45518d906f8f9b48fd7cb5aa0f4e2aff", size = 328333, upload-time = "2026-01-30T11:22:13.811Z" }, + { url = "https://files.pythonhosted.org/packages/ad/78/73a1031b7d1bf7986e8e655cea3f018164b3470aecfea25a4074e77dda73/pendulum-3.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:637e65af042f383a2764a886aa28ccc6f853bf7a142df18e41c720542934c13b", size = 340841, upload-time = "2026-01-30T11:22:15.278Z" }, + { url = "https://files.pythonhosted.org/packages/49/40/4e36e9074e92b0164c088b9ada3c02bfea386d83e24fa98b30fe9b6e61a8/pendulum-3.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6e46c28f4d067233c4a4c42748f4ffa641d9289c09e0e81488beb6d4b3fab51", size = 348959, upload-time = "2026-01-30T11:22:16.718Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/8bf7fcb91b526e1efe17d047faa845709b88800fff915ff848ff26054293/pendulum-3.2.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:71d46bcc86269f97bfd8c5f1475d55e717696a0a010b1871023605ca94624031", size = 518102, upload-time = "2026-01-30T11:22:18.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b0/a36c468d2d0dec62ddea7c5e4177e93abb12f48ac90f09f24d0581c5189f/pendulum-3.2.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5cd956d4176afc7bfe8a91bf3f771b46ff8d326f6c5bf778eb5010eb742ebba6", size = 561884, upload-time = "2026-01-30T11:22:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4d/dad105261898907bf806cabca53d3878529a9fa2c0d5d7f95f2035246fc2/pendulum-3.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:39ef129d7b90aab49708645867abdd207b714ba7bff12dae549975b0aca09716", size = 261236, upload-time = "2026-01-30T11:22:21.059Z" }, + { url = "https://files.pythonhosted.org/packages/02/fb/d65db067a67df7252f18b0cb7420dda84078b9e8bfb375215469c14a50be/pendulum-3.2.0-py3-none-any.whl", hash = "sha256:f3a9c18a89b4d9ef39c5fa6a78722aaff8d5be2597c129a3b16b9f40a561acf3", size = 114111, upload-time = "2026-01-30T11:22:22.361Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "portalocker" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/f8/969e6f280201b40b31bcb62843c619f343dcc351dff83a5891530c9dd60e/portalocker-2.7.0.tar.gz", hash = "sha256:032e81d534a88ec1736d03f780ba073f047a06c478b06e2937486f334e955c51", size = 20183, upload-time = "2023-01-18T23:36:14.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/df/d4f711d168524f5aebd7fb30969eaa31e3048cf8979688cde3b08f6e5eb8/portalocker-2.7.0-py2.py3-none-any.whl", hash = "sha256:a07c5b4f3985c3cf4798369631fb7011adb498e2a46d8440efc75a8f29a0f983", size = 15502, upload-time = "2023-01-18T23:36:12.849Z" }, +] + +[[package]] +name = "posthog" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "distro" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/20/60ae67bb9d82f00427946218d49e2e7e80fb41c15dc5019482289ec9ce8d/posthog-5.4.0.tar.gz", hash = "sha256:701669261b8d07cdde0276e5bc096b87f9e200e3b9589c5ebff14df658c5893c", size = 88076, upload-time = "2025-06-20T23:19:23.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364, upload-time = "2025-06-20T23:19:22.001Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" }, + { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" }, + { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" }, + { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" }, + { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" }, + { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pyarrow" +version = "25.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/e3/27f57f80141379d60defe6703eb50a707325706f07fedfd1312c7a751995/pyarrow-25.0.1.tar.gz", hash = "sha256:9150a83248bfed9813ea3c3af74c3856c1984d444aa28e58bf7733b9750ddf6a", size = 1201653, upload-time = "2026-08-10T12:40:53.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/3e/5cd70becb51e1d044c54ba5e627424a6e87df5b98008cbd22cc6abd409ca/pyarrow-25.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0b1edbb2f385a6a65e9711b62ba86ac54a7816a3f8d17bb3e8a5929d65fb2485", size = 35954271, upload-time = "2026-08-10T12:36:33.857Z" }, + { url = "https://files.pythonhosted.org/packages/64/be/17599e086df264ea7dc221d1101e3131e181e00da428a2f9bd0358f0d06b/pyarrow-25.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:a4dd8bf99a8fac133efc0ed6a92f5fddbe2adba0d0f6dd720e39ba9855cea85c", size = 37647543, upload-time = "2026-08-10T12:36:39.486Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/e138b451fd3970a6eda4599f68ae3b2b32b661bc958de3239d54a0bf6575/pyarrow-25.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bddd0c4f7630c2a3ddf6347c1bdaa79d97bcf6bd445f9e60c816b7d77c85a5ae", size = 46837120, upload-time = "2026-08-10T12:36:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/57/5c/f8fc0eb2de03464a557d5a4d0c15e972d73362414696618833b771f7eddd/pyarrow-25.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a4d6d5e9a3d1879a97c08ded0c797579b7965eafd0f0c26c30b45ccc06db939b", size = 50066460, upload-time = "2026-08-10T12:36:53.702Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d1/0dd64fd06de0333b808a02f60981635f067b71aad3a30698a9a104fae778/pyarrow-25.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:514ddb60285631af068875550c90eddc181db3e8e63a032b1559be189e82f056", size = 49937892, upload-time = "2026-08-10T12:37:00.349Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3c/f89d1bd76d5f3284c2a44d7d7ebbd8204535e5ae2b41f4077069b4ff2ec6/pyarrow-25.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cab40b1edfef0262e0e5251aa2c58d75630f24d06dd7794480243acc001a1d7d", size = 53107240, upload-time = "2026-08-10T12:37:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/b554a8e09f3f3decccf405eb8fbe86696321cbcb5b62d18b4a5057a4c113/pyarrow-25.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:60e89d8f13861a1f7f8d950fa54aebb8023b30734d0ac51ffa80beabe2df4bba", size = 27848683, upload-time = "2026-08-10T12:37:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8b/0d23b47702fcfe8b3618d5292035099675c5a1c48258932350c08020f7b5/pyarrow-25.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:51093dd9e10325fbdb3c10a2ae7c4806e5c822d94e74ae4938b26524a3323fee", size = 35946180, upload-time = "2026-08-10T12:37:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/707d17a5476c55a9541fde0db8213ac30979a792864d72415f176ba50c45/pyarrow-25.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:eb6203482ff3746a5632303a7279ae0b5a304c46985b49ed1378cb350ea6728d", size = 37644787, upload-time = "2026-08-10T12:37:25.795Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b2/cdc98ecf1a6408280bc3a6a07054cdd99a3f4670acc0545d383ce113e87d/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:880523be3d29efcf83d3998835d206118ccf35e3871dbd2fb60408cf6b007a80", size = 46834633, upload-time = "2026-08-10T12:37:33.604Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6e/d3fafc41f378b2c65be43b827798c0fae42049a641c8526633ed3eb573e2/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:25f8720bf6387d5dc2ebd2622112de630760419e4b66134405dd24110d15f37e", size = 50065507, upload-time = "2026-08-10T12:37:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/d5/12/8d0698954b8c3001844a898e0a6900bebe83d7ee40c11195174c5122f324/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4facd65742a024a4a366328a1d2292062d72d6e023c1b7dda8d4c37544933a25", size = 49955690, upload-time = "2026-08-10T12:37:46.644Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/1ecb936ac6409e90a34d58eea1c7cec09a9ae6d2141b9e49ad01a2b1ea47/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa0559502e1cd6254d6814614085dd9c5a3dd0419362978a936a3f68a9e5c3df", size = 53128198, upload-time = "2026-08-10T12:37:52.531Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/5236033550633c9b7377b2a53660b2bbb06cb06dc09c4356332d67643ca1/pyarrow-25.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:62cd0d785b8aa6675ee355f9fc02252a340f4441257c42674937826fd7594325", size = 27857263, upload-time = "2026-08-10T12:37:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e2/9ab15b88cbfac28e16419ce5439ec29234c5172cb8259301b4ba639bdec0/pyarrow-25.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:df961f2e7ae9cf496459259d798652c70625f6c080650d6952f8c04053c58ee9", size = 35861559, upload-time = "2026-08-10T12:38:02.567Z" }, + { url = "https://files.pythonhosted.org/packages/58/79/a0036dbe1eabe1f73127427342f1d99982584c4a2cde2651d6c93499c6f6/pyarrow-25.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:cc4aa407fde9fc660be3939e49ea31f50f3e9fec17c0ec63159f7711edd3efc9", size = 37628383, upload-time = "2026-08-10T12:38:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/13/49/d93a57d375f4bf0cf82913dd6bb54acafde83dd993be2282c81ac5616cad/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4340f0ba6c1d2e13f21658de1d7c662ca2545018568d0030a1e9afca159d87e3", size = 46820190, upload-time = "2026-08-10T12:38:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/60/c9/711ca85d79f1ec98f29a5eae2b051e25b4ecec5de3e3c0e2d5c5dcb15664/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5389cdf79447ed1515c9e31620e6e1e2302249564d603f2ad727d4f6d313e4c3", size = 50102437, upload-time = "2026-08-10T12:38:22.487Z" }, + { url = "https://files.pythonhosted.org/packages/80/53/8fb8359ff17cfb6263a1cf3ebf7caec9fe197de118719e84fcb1d0618026/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d51592cb7561e87877c506113e7adbf1342ab579e6c21f0ef44b8ba41cb74c80", size = 49942424, upload-time = "2026-08-10T12:38:28.755Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/4e5ae02a9341571b18a6fca380ac7a58ce6ddae7ab3c060208c0a1e79f02/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6109c94d8b9f3b17a041daca16cacb2f651ad8f1ef70a4232c2c0f37a23da2a8", size = 53144206, upload-time = "2026-08-10T12:38:34.862Z" }, + { url = "https://files.pythonhosted.org/packages/65/ee/197cbf47e49f83e6ebeb946a5259a48a638dea27ac774db42fe78022179d/pyarrow-25.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8858d7bfc22e3f51529aeaa4077225029724623e4595dc9eff8c793935c34140", size = 27953934, upload-time = "2026-08-10T12:38:39.808Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8d/8f271a7a034c834910ec925d56fa4b29733b1380f5289419f5aaa3b02777/pyarrow-25.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c7c534ec03c358a76ea3e505e74c1b6aef290af90c444dfd092dbfe23e755b85", size = 35855328, upload-time = "2026-08-10T12:38:45.489Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/5bac242f4e841b9971d5eb94fdfe2577e2b70be983e27401e72055786037/pyarrow-25.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dda9470024204d7bbf2042b47c6e8a0e47a3eeb8e34405882dfaea6577e0c153", size = 37622415, upload-time = "2026-08-10T12:38:51.107Z" }, + { url = "https://files.pythonhosted.org/packages/63/1f/96d03b4e1506524f7087adb0fd6b2f69f0c9c7aaff1ec36d8030082e15a5/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:44a9120ce5bd81936b8ab9a88076e3fd47c2c6838e0e43630fed83626aca81d9", size = 46813813, upload-time = "2026-08-10T12:38:57.773Z" }, + { url = "https://files.pythonhosted.org/packages/98/d6/33a411115b61dbfc16ad6ad73e71730f6fea654ee3667673bc53ab0e2fe7/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0befcf816e45a1af33ac775a9970b749e4868a230c7372f0ae5e932bee27039f", size = 50104452, upload-time = "2026-08-10T12:39:04.579Z" }, + { url = "https://files.pythonhosted.org/packages/33/ae/b1b97c9ca87f9f9ddbb5230c798df94eccce61bd79b9b45458c69a478588/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f89685964f46e4216103c75483aac0c0692a5f72212d7ca835adba5ede56ce3", size = 49951343, upload-time = "2026-08-10T12:39:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/98/9e/a112df5cfd5a68cb1d9fc31cfe38c28d5aec9f10865ce37ecef2e4450873/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6943e2fe7954d29d84de45d29d34c8dc36ce96570e67d89aa9976e650a4a9138", size = 53144784, upload-time = "2026-08-10T12:39:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/31/24/97e8bd98f1e3b07e2ba08bcdff690674fbe16d69a7d2712cc3884665e615/pyarrow-25.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:31e49a7888fcdf3a835da33ae777f6bb9a866334e5a789282fc26dcf426f7f15", size = 27870159, upload-time = "2026-08-10T12:39:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/36/4c/b525824ad3094076919273cd97db61fb3d78252dee76fa3b8dc8f76774aa/pyarrow-25.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bf0b672390cdcb640d7288f96b826d71ff4e9abb254a86c89890baf51a29cee6", size = 35885255, upload-time = "2026-08-10T12:39:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/448bb0e940de41aec31d1a956e63ad9c54afdf122a103cc3ab20c2a3ce33/pyarrow-25.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:38a9a4b4b9613380e200641891495a56c3d5a98a092db4a870af9975e220471d", size = 37644461, upload-time = "2026-08-10T12:39:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9a/13587e38bd4806fd218f50fd13b8903fab60588a699ff0c406372e5b4043/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b726ad7e7b669be982b0c71c07fe4b037d654354130da79a7902a669e93a66b", size = 46877146, upload-time = "2026-08-10T12:39:43.722Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/1c5d1229fa21da4cff5365e41e57177aaac57c563c727f35419b8513d1c1/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:9171748cdf796972d85a4b60157c279913e242992e350c90c7450182a9838b2a", size = 50131616, upload-time = "2026-08-10T12:39:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/43/20/291e1d65cc0b09aa19f03cf25cf51a2f5fa94b5db315178f2d254ed5cad4/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b7a296aac7a71fa0886c08e155ddb6c636a50013f801f6178daafa0f9e726188", size = 50008879, upload-time = "2026-08-10T12:39:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7c/1b7c9ec28e76576337e4f97b31141c9a181b89b6d1d6221e9d8205621a58/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0fe7c8b6c03969b49c8c66182e4a18e3819ab92d07cfab5d8370c531b9369ef0", size = 53170864, upload-time = "2026-08-10T12:40:04.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/75/f3d789dc06011a765d14d86bda799cf72ac1d715b6a6edecaa0d73d95062/pyarrow-25.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:f729cfdbd36fd99d543b67a914d2de044c84ebe45be8b34902b299b608c15c8f", size = 28620729, upload-time = "2026-08-10T12:40:51.41Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/647a8ee6f7c2662feb6921315617bc04dcd6034763fb61b1199720bf6162/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:59a2de54c0cbd954da861eee4d1d330f8e909c45b53455baef696380f2c55033", size = 36130288, upload-time = "2026-08-10T12:40:11.014Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/c9ee997554d7bea94520667dd1933f109ac1da3ee3556d2b49381e023484/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:35935cd5de130aa5cf4dea052a63e6bf2e17006c35c3a468194242b9b2bf5956", size = 37762187, upload-time = "2026-08-10T12:40:16.592Z" }, + { url = "https://files.pythonhosted.org/packages/a2/08/a28c01c7fe9e96e8233ce2d13df1d402f4f999f848f51d2daacd6bb4c036/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f3831aaa25c67a99f99dc8b05873cb9d64560390372e2aa197ce9dd4a3f06a44", size = 46888003, upload-time = "2026-08-10T12:40:23.242Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b9/58612e977d28dc58c878448866838369ee8da2f1e7cc8ed2c84b952aafee/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1fdfc6659b6b19022f2e50627fb5cf7156a66c46bf4299379955cbe742382a", size = 50079036, upload-time = "2026-08-10T12:40:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/72/13/66e1402dcc860e1dc2760b1e0292c9a569b62b3bccab69def1b3e907d006/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:169d3429d5be7c752125890620f75a60776d38b0035eddae939651640822332e", size = 50040226, upload-time = "2026-08-10T12:40:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/78/10/3f1a5497a7ef732ab0f03ecca3e66d89d9c0f57fdc61b4794c456b781f01/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:119297a6dc197e45d9c6d4415f7814a67ffa36c180d26f68c154c58067ae782d", size = 53149035, upload-time = "2026-08-10T12:40:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/93/c0/37d4a7e8e2f7a6076283673d5298018ca26478b934c6ee369e10505ab32c/pyarrow-25.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4288f27577352d608ca08553b0865e4a9b3aa14820c5d95b53337218d609835b", size = 28753071, upload-time = "2026-08-10T12:40:46.623Z" }, +] + +[[package]] +name = "pybase64" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/65/c513eab7211590250f729a06aacc0bc95eaf760b9235666e933d200105d0/pybase64-1.5.0.tar.gz", hash = "sha256:545ab2a433769e3b8e1ce2b4f7b07218bbde202f4954fbfe52948b2522120727", size = 149492, upload-time = "2026-08-08T15:42:00.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/41/575bcf8d2f67e32cfde5bec8356e4d42c5d2d67452d47d529ee4a2abd86c/pybase64-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30b0bc5add7b5ffbf9e8f84ad8cbbeeac420da70666d32bedecdbf2051e15592", size = 46808, upload-time = "2026-08-08T15:37:33.289Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/1b16acb03a19a898e7d96f81de4205c65e73b99b8c9f7237849e62ec0f00/pybase64-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:43885294c9e7c79c4a43c42fe759a82e92d8822fe3e7f2f8b23af90e5dbc4269", size = 40358, upload-time = "2026-08-08T15:37:34.779Z" }, + { url = "https://files.pythonhosted.org/packages/5e/80/3f0562eb2e8a84bbd895cf20b0fe7825f3ae6a37c11f281901cb5c739157/pybase64-1.5.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:32db63c2b2ebbd1152538e0c405bcb38bbaed1adba0efea04bd3d4b33e9cec70", size = 88169, upload-time = "2026-08-08T15:37:35.831Z" }, + { url = "https://files.pythonhosted.org/packages/1e/45/4cd44eacb152901c728eac25be8edaf261f2be7b07be690523fe9dc1b7c9/pybase64-1.5.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dd4abc5f83ea43fe977caa7111af763e0f2ad5f4143a55abaef8bc4efe4fe30c", size = 91528, upload-time = "2026-08-08T15:37:36.9Z" }, + { url = "https://files.pythonhosted.org/packages/dc/51/247e670ed36906dd0b7212b5bbe896b5d0f6d79061fdfb1118680c4816a8/pybase64-1.5.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eadf5e5fa8c0e2f15a3fe6f5513882f33b4a1b77d8c8cc9252c1e0dcc9e5bf6a", size = 81335, upload-time = "2026-08-08T15:37:37.996Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/a190bb18050f641a3b7d20a382c6f2f0b6d0747fc19220b9294a981af6bb/pybase64-1.5.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:305ae0210e974f5d0dad3f0052559a83297433412e6ba0f8a6aed93bb4083ddb", size = 77290, upload-time = "2026-08-08T15:37:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/42/ef/843ef314651c98e62d1a734fa444f462488e7663d6423d582aeafa4e452c/pybase64-1.5.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:282bd86c49ddd905bc9b8f166433b4e2e07f6130a273a5ca61c55f44005a263b", size = 79090, upload-time = "2026-08-08T15:37:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/36/58/fb24daf5a2d7e762528f60d992c7a37ec50e68945a003443af5a7808d531/pybase64-1.5.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f091c932bef000b8dff3ee00dfd8769e138021770d46d577168d802af7abd22b", size = 78107, upload-time = "2026-08-08T15:37:41.292Z" }, + { url = "https://files.pythonhosted.org/packages/cc/7b/aa7ebbcd0f9b4d50c8e83aaa1388d7e94fd63d84d35720b67ea865d4a87a/pybase64-1.5.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c7010b9ce91aaea5e389a7c4de0b8459a5a05a6795921124d8c82928eb13c4a9", size = 76113, upload-time = "2026-08-08T15:37:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/27/48/f7d8387969113a0b590de921fb5ab4d7e9d554a783477f15a7ab4056bf31/pybase64-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7ec51301e1f9f1fbdbd3bb6b34e0df08f5272937e0f86f535e9616341eb452af", size = 79293, upload-time = "2026-08-08T15:37:43.517Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/88e7c5043bbf459123b1cae785c7f0bd15fc3c524ca5e6df47b3170dc06d/pybase64-1.5.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6ab1a34d824efc0bf235c0abf9415256bbd74288cdfc47f6646ec9fce04076f9", size = 72740, upload-time = "2026-08-08T15:37:44.614Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d9/ee6825788704f64f37a4f8215f109985fb46b38f30e3791e2403f42997e2/pybase64-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0234b8f85c8816e82bbabf67a37014c3aaa2a668d3ab92fb5ef52c511318c84a", size = 87996, upload-time = "2026-08-08T15:37:45.843Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c2/33026e61bf216a2e9f65a0ab7de4ec504c6fbdac0d6597cc5077838ac9f0/pybase64-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a80226a2135de8a454e6812dd604d1c42c4e94269223b242395d689bf247824f", size = 76536, upload-time = "2026-08-08T15:37:46.92Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/03d706e2a1afa19e79d0b86170eedaf921e4889fe69e78c005892ccad3bf/pybase64-1.5.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:aea6ab63971f72f69b2cace481e0df9cb01486317296e7809a086a71864a6013", size = 75598, upload-time = "2026-08-08T15:37:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/19/4e/a29aab4757a46ed957306ed76004db8315de6da09535bbb4a34c213d9c1d/pybase64-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c3455b23f785486a5ab3d2b8bfc7f573d1bab0a10d061fb9b7f596096e316ae2", size = 75558, upload-time = "2026-08-08T15:37:49.466Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3c/92bd29e54206d300eec94f7b634b48f1de97e0ffc0cb70c481ec2751a586/pybase64-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc5b02c33ee9dee2cb3487c5d381bf931ff22144b1711fa093727fba991347ea", size = 90406, upload-time = "2026-08-08T15:37:50.58Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ff/ee054f2750b35727afd85a41a6d5e84be2130468d2ad10f7e0deacf64683/pybase64-1.5.0-cp310-cp310-win32.whl", hash = "sha256:352860c3c88a6ff74ed877755e20084e7645cbd5ed973448ca38f83c0aebc2ec", size = 42370, upload-time = "2026-08-08T15:37:51.721Z" }, + { url = "https://files.pythonhosted.org/packages/35/bc/ace0ad65a17acd0ecb762f5e1fb5c4a99a0bb02a2866ffb591e50b2ede05/pybase64-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:283d2fabf23e356e72b4fb8a59f5e319202c0328c748f6596f14459b0650bfbb", size = 44484, upload-time = "2026-08-08T15:37:52.766Z" }, + { url = "https://files.pythonhosted.org/packages/86/aa/3b21a1a11da0dc0e2e97199da211eb9939fce51fe6b98680777ab53fb897/pybase64-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:8e6afda6996523b29d42b8b9dba309d2bad53fd2eaa06189d735c8c7e2885455", size = 39906, upload-time = "2026-08-08T15:37:53.811Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/97242944a88812139762723196e27a24a1484535c7e614c808f513b6dbc5/pybase64-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43df778a20db59f231b02c6dd70958e1fad532fc8a4f6bebb0555e74abe01898", size = 46805, upload-time = "2026-08-08T15:37:54.941Z" }, + { url = "https://files.pythonhosted.org/packages/0d/30/c0471ba2ef6e15f153b8dd629c66f919f3f770c372c703a5637503282097/pybase64-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2615d10e4cad323925d2f7d904ae38c6ae439b33069a0d56cc4ce64ea4c9b339", size = 40351, upload-time = "2026-08-08T15:37:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/96c1413310f696ecd3364d887073f73708121469ff7f3bd3e24ec8dece6d/pybase64-1.5.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:045fa2f3f5da6cfa86822c645b92e18cfc7c13babccb5ceec9bb64a17ac3f1bc", size = 90662, upload-time = "2026-08-08T15:37:57.091Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ee/591f24aef04e1ca569450b85c86df0f8a8b3dda04f68cdbf6101312456d7/pybase64-1.5.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93bc9bdfaf87dc7d79ee0182b255383b7f82a3167d0166b99330d897b59f9053", size = 94201, upload-time = "2026-08-08T15:37:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/20/c0/66a721f8d0d5f3d43704b78b30ddc51d07eef24ddf94470c8e1808b0826f/pybase64-1.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b08e4a065c9fa88ab9b8a2345b58073776806488b1ff5e4348957d0aa218043", size = 83843, upload-time = "2026-08-08T15:37:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/44aa61b4738b9827e50fc081c9072d553fc538a372580a6326771848d1cd/pybase64-1.5.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:897ca382ec6c7bad041ce7b3a64b3a15f1b639dfea89ffcf29bdd235c706fac3", size = 79555, upload-time = "2026-08-08T15:38:00.942Z" }, + { url = "https://files.pythonhosted.org/packages/98/f2/db862e347968eeecf96729244f092a8e1d9bbc1daf94aef4baf3446296d5/pybase64-1.5.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3398eb35a82a94d61756f7a4ad6a1c5a3e735c6abb97167398a22389a9b8ca7a", size = 81765, upload-time = "2026-08-08T15:38:02.206Z" }, + { url = "https://files.pythonhosted.org/packages/86/37/2ec5e90db7c1d01c126b02933adb31838ed8f4d8834193c4f2c1440e2c28/pybase64-1.5.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c3935b4402f257d9c7448944db07f91d6fc20453f8c3f0fa1bf26c490b534c84", size = 80619, upload-time = "2026-08-08T15:38:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/9d/80/09093682d7834a0cf8516b4d9b2b9ba579abb54c56046666ab12f97208d7/pybase64-1.5.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a167f17421c237a32c93072a053ff756d9fb225e69a620c3f4818665f0520044", size = 78240, upload-time = "2026-08-08T15:38:04.484Z" }, + { url = "https://files.pythonhosted.org/packages/20/6c/57d95e6ff206d7aabb0a6ec54a8d860ca1e0c84fee611eacfe95945b7478/pybase64-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:716aed288780c9c2081943a3a7b5be6993cdad56b0cdcb4ef4b562ef56c5a1ae", size = 81888, upload-time = "2026-08-08T15:38:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/d0559d16467c42ac91501e5e423d65be1801dd5ebf40f4a244134c760a46/pybase64-1.5.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d373b682dd0a267ece21869ca9a48d40b55120a3be714661ad0e9afdce9ce27e", size = 75233, upload-time = "2026-08-08T15:38:06.645Z" }, + { url = "https://files.pythonhosted.org/packages/40/2e/4dfee2d5c37473cb91204dac4f1df83710da30fcd93ae345c2a5acb6bdb9/pybase64-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5d02948944dad3e99ebe70a3049d7df66f5faba97ed03b411349b034558ed936", size = 90524, upload-time = "2026-08-08T15:38:07.791Z" }, + { url = "https://files.pythonhosted.org/packages/8d/07/e2595a9b32d2e635514de93847e1dca2bd6361c34b2e142b46b0eac852f0/pybase64-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d83f517403ff39404b8586d07e97c019cb2a7cb6665cb070c6aebf1fc03e5487", size = 79217, upload-time = "2026-08-08T15:38:09.004Z" }, + { url = "https://files.pythonhosted.org/packages/13/ce/57bc5269db8cd07f427e7b42149f00194106c80d02f4147411005dee7522/pybase64-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:216b78caa73ae9b82f3f006e9694ee5a1bde89e50f4552fd1679b56b080cfb7e", size = 77807, upload-time = "2026-08-08T15:38:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/bd/08/f366686f58858af2ec5dcedb80c394bf264927ba12c99d9a7233cca16c66/pybase64-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0855f67fa47c0bdf237ea875c11afce2a8cd879644b288d3f05ed9effab17953", size = 78181, upload-time = "2026-08-08T15:38:11.321Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fe/de44b16a234b12adf2a2b26a758805de8e7a88e2ccd547053d3dd96d8c57/pybase64-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a707d36935229ae5c3044cd601908cb7bd9f25757003d029765ccf66818301ce", size = 92950, upload-time = "2026-08-08T15:38:12.464Z" }, + { url = "https://files.pythonhosted.org/packages/25/42/b2b1ed374bf93defab1d3482713ff76a9e56b61606eca5853c10a8ff5bf4/pybase64-1.5.0-cp311-cp311-win32.whl", hash = "sha256:e868946a538178990a43fa6bbeff1eb027e515d6269743e4d31d19f72daf00ac", size = 42377, upload-time = "2026-08-08T15:38:13.598Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/3e0be8871e71f05fe98074f40bd5749c17567e01b2c998b1fd88530eba38/pybase64-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:49c62921f55d9d7713faeb855bd9aad1edfb8e09e2c8133b7058d4c447bdaa6e", size = 44491, upload-time = "2026-08-08T15:38:14.646Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f8/114f6da0f71003a6826fe30a24096e98eeba6c506b1f9cebb2689b01305a/pybase64-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:8dfe4566d653684daa21f41c75c8a64a8333b36a4377ccb12a1f16e321d7d1ca", size = 39913, upload-time = "2026-08-08T15:38:15.744Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/dba60f937caf26a6e2be6a138f5422da9f4ec988db49bd4e329bcb435cd2/pybase64-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9732eba18ba7fe44c1b2827bfaadf381fed3789bd7e20c990e6c8d1ceba0179b", size = 47155, upload-time = "2026-08-08T15:38:16.705Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/302d65a981c9baf156e4becbbbe49f38de72906c430ab373d6d1ca0d4258/pybase64-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d1149b7360dd99ef1ad10618df2a4f54a00385bc8d2c1aa244c0301a548ac415", size = 40490, upload-time = "2026-08-08T15:38:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/1d/66/9f1be6a4db86577eebf3106496a2a791b37e5fb74695d4c8eeedbd04490a/pybase64-1.5.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:80b171f1546935be4dae1e01bfd8630d2712271e067858b7135726e7d9bc7cce", size = 91058, upload-time = "2026-08-08T15:38:18.983Z" }, + { url = "https://files.pythonhosted.org/packages/af/36/4e44a0688efe26434bf378b4565b01ac94f81422e8a5746291a03472cd56/pybase64-1.5.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1a2b9cf39b4d30f600df8c56cccbc03adfc6e1ae8c04cd6b181105a432d4a515", size = 94681, upload-time = "2026-08-08T15:38:20.59Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/fc02005906fd48081b7b8f077cd422a55399fa351c2a6d3e5fed951794ce/pybase64-1.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:865b7db127a95e33640ebcdb4bb3aad165d4873ee7c1008949129f3c4f900dd8", size = 84634, upload-time = "2026-08-08T15:38:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c6/5bb0f21a9f4d231339a42f16ebabc7c6d9a7d619e756327b15a474650ece/pybase64-1.5.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:3344ce336d9d8292125369c1475d1663e7e1a06894e8e5150307e11f782c6afd", size = 80455, upload-time = "2026-08-08T15:38:23.05Z" }, + { url = "https://files.pythonhosted.org/packages/b8/04/0ba9a1f2ea39baf081dd44d22d710d9b050ce15991d641982f1814508484/pybase64-1.5.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1aaae81669bf18b5a35dcb43dbb200f52b13f847a56bed7a2e82f31cc6f9f74d", size = 82304, upload-time = "2026-08-08T15:38:24.156Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/6b380ff964dd77b79cc1ce565b73780345132e0e181d315f31a2263c5e1f/pybase64-1.5.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fb5dc922ce3cb4211caa7e29e6daee98f319e59f297a904acd74f2fdd0674356", size = 81259, upload-time = "2026-08-08T15:38:25.327Z" }, + { url = "https://files.pythonhosted.org/packages/b9/93/dd7fd7f8ed228f7735ec59a9f85f3c683cef371a76b29520344655bf7c97/pybase64-1.5.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:356e7bd1453551c06231df8411bfbaed9998fbcba2da723d84fb270ff1f977a7", size = 78360, upload-time = "2026-08-08T15:38:26.678Z" }, + { url = "https://files.pythonhosted.org/packages/d8/99/b5e9e7d4b5e49d7a984c4a26b48bdf988ec62c2778df80144af1a39bd4b1/pybase64-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:11dfa286f6c5fe6795430bf08fc44b64c98e208558215b0590c9f28fd99a92e3", size = 82358, upload-time = "2026-08-08T15:38:27.856Z" }, + { url = "https://files.pythonhosted.org/packages/67/fa/19d11ee70fbdb10e574a39ad7fc7adc06e5635a2b2ac291a6554c7c651ae/pybase64-1.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6be40c3311eabe8a816e00041844f9b249828015dc98be8a48a7c3275954ee76", size = 76384, upload-time = "2026-08-08T15:38:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/71/32/a83622dfa3162dd6fcd019dd8fbb766f0ce064fe67b3d3d2759881dbac4e/pybase64-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4e8b163c8d2d2a5f414f2c31cdd91024e0c91c72e735a9a564a62460ac838acb", size = 91407, upload-time = "2026-08-08T15:38:30.306Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b5/1707748813784af0b1340f6c6525887f1ecb393c3f88070a2bb2d86bd94e/pybase64-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0030a64fe91791e5e553edaff3a55d319cd07fb5e097b09c5f7f45e4905c40cb", size = 79687, upload-time = "2026-08-08T15:38:31.771Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ee/8101e43b5cc070c0adf298f87500154c13b9097d4456a2c1aadd71339329/pybase64-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:28d5db510433bb1544dc128c4e7ebd85ae57cec2a4608edd1f7ca4fed3e53b3d", size = 77913, upload-time = "2026-08-08T15:38:32.898Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/43b2281077ca9a531bd896b7a9fe871d091d80d172d68e439c7aa6337033/pybase64-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:26422429a0bb2f15773dacc0fcb1bcddfce68c6b2d41fc14bc7fc17f8c529542", size = 79172, upload-time = "2026-08-08T15:38:33.974Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1a/b536e571518eb2f4a2db1c6c7c5913af5780ff82c9eefb41f674fed71ceb/pybase64-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ccbae849677648be456ea0de769a78e432d2d24f71cbdc739741e69f8160e0d7", size = 93636, upload-time = "2026-08-08T15:38:35.102Z" }, + { url = "https://files.pythonhosted.org/packages/54/c0/318f79b614fa03089bf4672194325dfa732790546530697b55a53612637b/pybase64-1.5.0-cp312-cp312-win32.whl", hash = "sha256:d691553d1a88ed87cf1837babec3663275b29de906b48433c15b298e262e5243", size = 42443, upload-time = "2026-08-08T15:38:36.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/80/eecc05ebac8d08a2bf855cc7bbe6a37d8c76cd19c6337c9b9fbe3225ee19/pybase64-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:125945f5b3cde8b79a8f942cfdb0390f4388fb9458a41f5f2a93746e1ef3c546", size = 44565, upload-time = "2026-08-08T15:38:37.734Z" }, + { url = "https://files.pythonhosted.org/packages/b4/87/193dbb1eaf7751527a7e0510f5670efeed8642ec647b4c7177c384a6f7e9/pybase64-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:c8b5f52776f0277e72a9c7e7944f682de2b3ee4655b7972a48c53f871963741a", size = 39918, upload-time = "2026-08-08T15:38:38.808Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7c/b359e979a2b53f1aa9d8f2d9f90b29eda90d7dd126c2871dc49db4d6d8cf/pybase64-1.5.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:2e79853f8e52ab0afa7b3ae445de23767b033fa0e58ad11099d3c6b79d012c7d", size = 44413, upload-time = "2026-08-08T15:38:39.883Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9a/7412bd0e2c011069c754a1ac3e05ded9eab56614eea6d9251c74a434a472/pybase64-1.5.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:7661246f93c902bf147d5f7d72874902ef3e49a63ca3f0de333cb8e85765d2fd", size = 49859, upload-time = "2026-08-08T15:38:41.048Z" }, + { url = "https://files.pythonhosted.org/packages/a5/17/a1fc8e55551530876d3be31079b8701b7f5ac8451b63a08a19a4f9714454/pybase64-1.5.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:75d21d0a2cae0bb071c68686d77e5100be611ec4e80e0d97f8736c27da0ab197", size = 39681, upload-time = "2026-08-08T15:38:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2c/b46f7e0c1ea482db0f8445d5bfad7e5a4f39d977868e10b4c3823e94fa20/pybase64-1.5.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1bde27266ec4a56c38ef8e17998e430d30cc6310fde76332381bf5aaa81872ba", size = 40200, upload-time = "2026-08-08T15:38:43.354Z" }, + { url = "https://files.pythonhosted.org/packages/da/12/085dc70e757e6101c8f61239bae538640aac60ddfebb41e2534af3712e14/pybase64-1.5.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:220d8ab003d44144d80f8b776019adedc23fdc7bcb270396744b9805a8186d0e", size = 46726, upload-time = "2026-08-08T15:38:44.378Z" }, + { url = "https://files.pythonhosted.org/packages/60/7b/f3213973e61b8a8d1bb78203fe226e7f368698fb931249eacc09048d2141/pybase64-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d42196f594460a083084d8e3c2f2554c958ebd8fe19bc30ef1b938197436e7d5", size = 47242, upload-time = "2026-08-08T15:38:45.478Z" }, + { url = "https://files.pythonhosted.org/packages/1b/64/e847e8710261596b3e7cf0935041a1c96a50fb2a7f3e9e09bc495510b25a/pybase64-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa56c549af248664ed7e1cc8ebc4dd7f1505b1444d8f3bf15b6a89b43dd4151f", size = 40628, upload-time = "2026-08-08T15:38:46.597Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/8171fd18a57218c5e7c252f658709f9bd3d0eece9d4196542230103a53d6/pybase64-1.5.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a1529b8e08a93dd9c00d1e3b3c2b627a9600d96c2f40143dc0b3a85f48fa85e5", size = 92183, upload-time = "2026-08-08T15:38:48.038Z" }, + { url = "https://files.pythonhosted.org/packages/23/84/b91aabd22a65a3679633855dde720dfb86571e15f88a9b1b295adda90e8c/pybase64-1.5.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0be37689b624ae293394fc826c9a048c6118520d6a962de033ffb054564bf61f", size = 95718, upload-time = "2026-08-08T15:38:49.104Z" }, + { url = "https://files.pythonhosted.org/packages/67/cd/441fd3b9bc7a49846362fb52a0971cee6da4dca2eb8545100ec043b2a0da/pybase64-1.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bf98b77c6cca5c5da30135b69b30668da07a32d41210c62121b34c84239d9d4a", size = 86068, upload-time = "2026-08-08T15:38:50.683Z" }, + { url = "https://files.pythonhosted.org/packages/2f/24/48cfe7e1b776c0af1ce5240f7e71383890cd361242e537b6c510804a68d2/pybase64-1.5.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:0578c54f1ae89e6175eddb742dbaf2e95a060735ec11f4b661f762b635680cbd", size = 81077, upload-time = "2026-08-08T15:38:51.825Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/5b47895e2f19f9775a3daaec98a652ba7c0ccfb480c223d981c2ec75c0ed/pybase64-1.5.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ae78cdaec57f21e7f44cc5f9866d694cc072e1b1082286f30fd74e7545fa2916", size = 83387, upload-time = "2026-08-08T15:38:52.921Z" }, + { url = "https://files.pythonhosted.org/packages/74/2d/115526e63080e96ce039619a1a29a4fe49d138c5d7d525b6adbccf0c1c0f/pybase64-1.5.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1f315f07b269f074995c445b65dfde62d12c0e889e9c3b0534befdb05866e880", size = 82460, upload-time = "2026-08-08T15:38:54.436Z" }, + { url = "https://files.pythonhosted.org/packages/53/b8/8970ecca7a5945f81d34f9a91d23169f7e62e2487ef3694e0004943e7243/pybase64-1.5.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:99570e43605b9c849ff1606e1691e503962250f80ec3e827249f7ad820e402d8", size = 79359, upload-time = "2026-08-08T15:38:55.69Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/eea9cb5955430d5f789c18eab854284c66b1a024efae4928992d44bcde65/pybase64-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e0143b3515b97bb3c4743fbdf10f53950c0bb1fe1a2db1054b422ba370594333", size = 83768, upload-time = "2026-08-08T15:38:56.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/a121c58260d63d16861fd936373d07c4ab0cef51b0d7391cafaf8e4648c0/pybase64-1.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b0597ca31c472f3071844648ce5ab86a1732033ca230daffd8f87c6f8596a8ae", size = 77416, upload-time = "2026-08-08T15:38:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/24/6a/ea3a1078de626ce765402d6d3e1cb6d69f83104646bcf2e2772983be77aa/pybase64-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8d303baddeddaccada149bbee270b3e2eedcaec2df082834895cdd897a602674", size = 92473, upload-time = "2026-08-08T15:38:59.149Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/22878279f1663bea15b5211056e3c8cb19c4783d2566a0032bcfa37d678b/pybase64-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a34261348f88443d9e234f251a1f1fcb711c1cc006824fdb29b649735d8ac35f", size = 80804, upload-time = "2026-08-08T15:39:00.271Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f0/57c36867282341ccc47c0db67590dd8f0c621fd435aa5944bec4713138b5/pybase64-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e675b15b7a7b81e5b1a1e747cc49f9f9e6649d3b5e8a61719b46b9a671433210", size = 78871, upload-time = "2026-08-08T15:39:01.429Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ce/23b80fde747156f6387a2f769fac1384e2e34cd4f07daa32e990991eb64a/pybase64-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1f8f1bb4158069291fe6ac2d34db942418f2804564d04b8e97722041035f843", size = 80451, upload-time = "2026-08-08T15:39:02.764Z" }, + { url = "https://files.pythonhosted.org/packages/bf/02/1486ad47fc065bbaa45c12229673bb03f0480dabdba408b04a54ac480264/pybase64-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0abc0f2312c17765bf92dd382982cca9dc1b0148bf0d708f5f88339d84bb7687", size = 94725, upload-time = "2026-08-08T15:39:03.877Z" }, + { url = "https://files.pythonhosted.org/packages/43/ec/bf6a0df18b4a627a2ad3c8897e67797cb8128fed8cda2b654dd9ddebba25/pybase64-1.5.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:92998479a2a4464d141ef709e52dc3e4d4d4ce7f3b9cb5052d2c56c55b405b15", size = 33074, upload-time = "2026-08-08T15:39:04.939Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4b/58a70d9655842161bcc3ae73efede60ad83d6d195fdf110f0c0ed808bca0/pybase64-1.5.0-cp313-cp313-win32.whl", hash = "sha256:91aceea4287299ee60c1176909efd6f2de091da24c0d93d2f9861c93e3776ef7", size = 42557, upload-time = "2026-08-08T15:39:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/ba/43/157fddaa16e53e50813dc73b2cb9e4d03e797427394657e89e14a1a8843f/pybase64-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:d01e4d495c5b10e79de3449501e41d2bc2a4aa90844a3735eb962a3a01645971", size = 44628, upload-time = "2026-08-08T15:39:07.067Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c5/b5814d726d05749e6d5343a61c270a3c14a1f41faa20f4044ceb4f96d87c/pybase64-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:1f7ddf3a7f1c85061f246a481c63a70d7aadd0a49add8e6c109b65360fbf923e", size = 39953, upload-time = "2026-08-08T15:39:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4f/111bc52f03b44d569af3988a0665b2747ffe0e2a94008d03c976966e962d/pybase64-1.5.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774fe1a69e99c60ef7f5fb3d688e85db707e232355b4c93bbb96b4d17c5503cc", size = 44417, upload-time = "2026-08-08T15:39:09.252Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0c/432c08ff8dad0b08035d1aaa85afedce263d321cccddd6d63282aa736800/pybase64-1.5.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:b813d6eda1805d7d8acb176589ee1a51c4d0e5e3245093eddbd330d6508bf112", size = 49866, upload-time = "2026-08-08T15:39:10.78Z" }, + { url = "https://files.pythonhosted.org/packages/92/41/cef45112b1c853c58a5a47dc4fb823d1cd7c79cf24bb8424ef7fd3fbb180/pybase64-1.5.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2b5563aca0b7b74751dafe6cc3e1850a3401414c05342f1bbeb26549b5c3bda0", size = 39687, upload-time = "2026-08-08T15:39:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/9058281862be3a2a12b1b2bd48addf8e0eaa085c1cf75e22d49663b22a9a/pybase64-1.5.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b6cb9e548816e0838b10d29b061cfbbfc81b726f6c5f89d60e83bd7d703ed06b", size = 40208, upload-time = "2026-08-08T15:39:13.588Z" }, + { url = "https://files.pythonhosted.org/packages/60/f0/f6ff0e564d4d2f4ac9161d6a8445cbfb317c83ad9f79deca3c3bf27b8b79/pybase64-1.5.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:435064ff2fc778a02d1234289a22050a4d3b29752062b5ecaf45eae62273ec47", size = 46726, upload-time = "2026-08-08T15:39:14.689Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/fdf9e6e71090a1e31fb4967ab3042301ab71ceaa800f3b1805be29e4dfd4/pybase64-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d7c77f38e6d0b5bf8d7af9cb9c6bb9f4e62f25edc2931251d46c3ed0d89121ab", size = 47350, upload-time = "2026-08-08T15:39:15.798Z" }, + { url = "https://files.pythonhosted.org/packages/22/39/69828d263af0d31c8ca99d7cae4cf8a5a9f37a1bfc63f2a40afb9cd2a805/pybase64-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3930278a6635dac4dff15f8f336ae643101608160f4525e67a9fc8416061daf", size = 40647, upload-time = "2026-08-08T15:39:17.019Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/cce345652b019e2a80e51b8d31bd4fa1662612ff1260dfedbcd5e1675106/pybase64-1.5.0-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:fb1734c69974acaee369726b48031c0d0117830bc050188086a69227c32d2426", size = 92385, upload-time = "2026-08-08T15:39:18.169Z" }, + { url = "https://files.pythonhosted.org/packages/86/0f/c332c26d75b0f2bcab549fe746b6978b2928d8b94fe226333c7e94ecfdd1/pybase64-1.5.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b391e54bc8198387cf089ffd343d8c99d58e73f209c31aa2e5f420bf20bbb0c7", size = 95720, upload-time = "2026-08-08T15:39:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e5/0476f7b28544d29225bc1b0be5fd613ab62c38080c65d55299a8f1e7e334/pybase64-1.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1626f1de1d7c109e25e20528cf1ffe17d0b614baa87c9d20f6181cb65234168", size = 86226, upload-time = "2026-08-08T15:39:20.473Z" }, + { url = "https://files.pythonhosted.org/packages/75/5d/5664794aff60d8df94371a466171940c3ecb081d76d24ca1327dd32aed60/pybase64-1.5.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:ade98a94cd71692baf0ab21245ecf9a2f1c275460dc4106e23ce9aca1c4c1838", size = 81080, upload-time = "2026-08-08T15:39:21.899Z" }, + { url = "https://files.pythonhosted.org/packages/f3/32/6ce14f3209f1629e11b11f1c44f545b87ebe88a2f35e469526d72f2fe0db/pybase64-1.5.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:cbc41c5376b30ba7b3d558505f7598799034c8aef30e3cee00f32bf8d26fbede", size = 83557, upload-time = "2026-08-08T15:39:23.429Z" }, + { url = "https://files.pythonhosted.org/packages/38/5e/0d73f7f9d3e4579df08af94847e39a675b654b4c99330ef1b5718594406c/pybase64-1.5.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be98a4e72e3821714770ed290e5e1a8a6cabe77af58520a9adf718acc43a165e", size = 82370, upload-time = "2026-08-08T15:39:24.696Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f4/0831d2736370a4afc690aabfe60e295e2773456efdc764513974f7b2b2d9/pybase64-1.5.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a8bc9cb80cd736785aa39be5e5d934772a36f9ba30fa71b7c19dbe1da44a306f", size = 79538, upload-time = "2026-08-08T15:39:25.859Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/c897af35bdc3f4e26d1050c8ada1eb91dff87e601681bb6b8a3f47db6b42/pybase64-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aabdeccdd1be80735cd8cb815565d9528c767113358fac2e8eba21030e018a65", size = 83889, upload-time = "2026-08-08T15:39:27.024Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/89be4c77ebbba058ea1d263a62349d306b13d92626b2593c4b56e01321d2/pybase64-1.5.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9d16bd1cdbb63985cb2f3ec4bda4de13ba6396c1f81468941c650b4157670ee1", size = 77092, upload-time = "2026-08-08T15:39:28.573Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fe/eb0723048975618b73f3dc4b3b4e906b17aacd50916f3de3350a9980fbfc/pybase64-1.5.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:37daeed30664d0d59dc0c99707a3a3fb723b8dffdf62266078308b9b26c7a18f", size = 92793, upload-time = "2026-08-08T15:39:29.739Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/a6bb0ac9aa970322ea30b37af8054a8110d2422cbe7bcaf99cee110d77db/pybase64-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:40fd8e16bfde1e9d80700bbdb51a830c0f7e384c2130c4a8ed5f0912fb269cce", size = 80873, upload-time = "2026-08-08T15:39:31.017Z" }, + { url = "https://files.pythonhosted.org/packages/6c/00/75c2ccacfc7bd47d50bdc91fee3e09582ca9bf047414fcd44ed9d61e55a5/pybase64-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1c32f2078df7e3c4f7e573592cdcd8eb50c827cd51226291ee867c217f036abe", size = 79031, upload-time = "2026-08-08T15:39:32.269Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ae/f6cdaea0a266cdeb485f6088551b8413361947f008de21ebe1479b5c5042/pybase64-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a119cdd2e59b30aa570e75182b22fa149da50e921ed8b4c492eb9ed308d944c0", size = 80520, upload-time = "2026-08-08T15:39:33.463Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f8/7961ef761d93b1bca865dc84e99ce071f73b05a8f73e2759e19b42732d1f/pybase64-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:82b38c11b73d4ea37b1d76d4690131472ce6a144166a63fedf336d88a101336b", size = 94804, upload-time = "2026-08-08T15:39:34.589Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/4dcf99fb78ed8cbb5c45d4a4580ed7d3206ddf098f4d9bb03f9f292c3e7e/pybase64-1.5.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:6260074fae5bc47838af0fee1a6f48530d1ac7b5f49c80868144ba2f69f43145", size = 33041, upload-time = "2026-08-08T15:39:35.771Z" }, + { url = "https://files.pythonhosted.org/packages/21/74/e62ccf872d585b6a9a557ee9b722a3e719fb9b478bd6aff8e741d7d59470/pybase64-1.5.0-cp314-cp314-win32.whl", hash = "sha256:1003c3643cb785b90237c9fab9163dbb349b17a774f9421488a2147f7382c134", size = 42559, upload-time = "2026-08-08T15:39:36.827Z" }, + { url = "https://files.pythonhosted.org/packages/45/32/901b374323483332e7a5c4a999a571b853e945e780da0223438b3d4a7220/pybase64-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:4bb9dd97bdab9b6ba0e80f9d83e140e8263567d28878fcc52f8f0f41990926a6", size = 44674, upload-time = "2026-08-08T15:39:38.16Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e2/50c2b5939360b6682742fdc6d12e3a9e37090b1d25206d25131b66e61238/pybase64-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:216a160461168c12c5ec0d6384a0dcb73e7b3c392df3e30c1fa11cff1cc8be82", size = 39982, upload-time = "2026-08-08T15:39:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/8afa00f5daa954c1f9975477648cd1c55c5c7eec2a0b0a963323ca8da286/pybase64-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fbf8e901a9caf045062b7a1a8f7db056c492a5a76a0c612714ed7abb5ad42f7a", size = 47688, upload-time = "2026-08-08T15:39:40.754Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9c/10bb03155f5dd605befde5b8c5f9e867b5f2b885fc3e4afdccbda02c8c0a/pybase64-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f5b5f72a0d761849c75b0524606707b28600eb9bf75263e7f36a7ca33627fbbb", size = 41021, upload-time = "2026-08-08T15:39:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/86/69/54f004e0f5ab8e7a96b1a43198e2fb554c2a94c4b78f553ebfef733377b7/pybase64-1.5.0-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:0872880c9150fc79347c658937507033b8e600570569e4494e1230987e91be04", size = 97305, upload-time = "2026-08-08T15:39:43.093Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cb/d1d33080136e437e49d73bad2f27a1ac3129b058585c71fdab2c8783fa2c/pybase64-1.5.0-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:106dc1813dff9ad1e936ab6de486bc0e19d281741c1cdcb3effe31602c571d71", size = 101290, upload-time = "2026-08-08T15:39:44.318Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a0/b935f321d7f4e6317487b9452492287f4b709465290445bb8daa104d5264/pybase64-1.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c1a3279af228faca3c224cc8c30aa130b5f3184ba420ac477de1db2cb99be8a7", size = 92538, upload-time = "2026-08-08T15:39:45.863Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/5775be12186b4bdadd9b7bceea9871af097547d5a2d8bec4e43ed9d5408e/pybase64-1.5.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d8e05ac71573089f25cdbad4b01db8d0b8e82846cd42291ef002d265903b1e41", size = 87146, upload-time = "2026-08-08T15:39:47.04Z" }, + { url = "https://files.pythonhosted.org/packages/53/0d/15b1ff749dafa2146a0a7aabff8596cc6bbb4277fb90f56a0beca9cdda92/pybase64-1.5.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:08907ffbf8381a017f6332ce02b818e672c73563ec19f38a022a34fd1c55b493", size = 89500, upload-time = "2026-08-08T15:39:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/8e/dc/bcfc83c650a83f814235c56c810570fddb382a23ad3f79c6816e0c9b4351/pybase64-1.5.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6a5f053f077aa8f0ffe5d4d03dd7d3fae4b85155942228a6dd20b467c4d7d80", size = 88169, upload-time = "2026-08-08T15:39:49.455Z" }, + { url = "https://files.pythonhosted.org/packages/ff/43/992c2aa344020575b0539c388104cb9ef45c80429f99acd0f177d32bcce2/pybase64-1.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1e149af6b5a5af697725abc52aefef7e3ab036f21f5c229848b0f8bc8f26edee", size = 84770, upload-time = "2026-08-08T15:39:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c5/da628bb02323057cfb8653b427fb3b6c363a395e954dff38c512fdfeea56/pybase64-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:678cf90273ee5fa7cedb35334c765ced4dad38608c0258445da009c1da9dd174", size = 90101, upload-time = "2026-08-08T15:39:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0e/5824a07fd5f64e80ea042624f0e2b03cdaa3ce786e201c02746b2552d4ff/pybase64-1.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7dc71ba89766bef4bd2d9be8a827ce784f1c85915b8bcad2deefd7d892d6816e", size = 82843, upload-time = "2026-08-08T15:39:52.892Z" }, + { url = "https://files.pythonhosted.org/packages/b8/21/fb0e4da0de2e5bbd3a9bee14c6919550a6ebdb7344776b7730960a8d37b5/pybase64-1.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c6b6c15473fff013dfcb0b89cfcbc922442459b08e96d37cdcf1a8bec28e4ed4", size = 97635, upload-time = "2026-08-08T15:39:54.095Z" }, + { url = "https://files.pythonhosted.org/packages/06/ce/c382f3401435e04a2440cbc6beb7317278baaf4e2fee28846325446f669e/pybase64-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:831c25fd670727aea65525b9d6cff00718f26ca92433f9ed039fe67af9825388", size = 86714, upload-time = "2026-08-08T15:39:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/a1/90/f0552285b2e17ae405e675debf3fa6b999622b1cc3f72584b9cb3904584c/pybase64-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f2509dc39574f1a0c60eb5f6c968e6f064b55bea88506df25d15ba6d391b1c48", size = 84316, upload-time = "2026-08-08T15:39:57.268Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d8/66b3468adf62bee0c27cf39aa1f2da6d9b2e79f01d25c7af0310997502b4/pybase64-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fe57aab650c771802cc7b0eb541a74b6a181cd1870f61c537294ab462fec34e8", size = 85344, upload-time = "2026-08-08T15:39:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/8fc0795eeaaed8a7d29068e465aa60516891e95854e3ef23231d28bc0766/pybase64-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:816fccccaa736743c19f8fd687def788e0c0813f8168f88c4d169827b6726d65", size = 100106, upload-time = "2026-08-08T15:39:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4e/8c7ce1a990e44598b97540b4dd92bf511190da6663d5c4c76c58487279e8/pybase64-1.5.0-cp314-cp314t-win32.whl", hash = "sha256:a18c7dfab52b07453321b24e5be2d532e7875076e67b7295b5b471988616b541", size = 42948, upload-time = "2026-08-08T15:40:01.078Z" }, + { url = "https://files.pythonhosted.org/packages/af/48/006c6c76f7957dc08c06b5057bfd85f7e2bfefe57bd7719de6e80eb30cea/pybase64-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0d1c371e90556712ec937ded4fe1986176e01ce9568749f98c562115a427ab2f", size = 45268, upload-time = "2026-08-08T15:40:02.229Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/21d5b0cd2350e4dbee886e459acd4aa0a980086dd211458b185916e9bdc0/pybase64-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2b01763ed71f190651fe53faa1ec5e41ed8d6c730d0f32f25da8afff07b119", size = 40275, upload-time = "2026-08-08T15:40:03.466Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3a/1d8a9bbcedfe9bb52eb1e67527e0d11b7a5c4e1bb311924b3b181211cc30/pybase64-1.5.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:20e4c838a84fad3491027f0bd364f6fe21eedecab51860078b23cdb22bcb016d", size = 44702, upload-time = "2026-08-08T15:40:04.648Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b0/97bf3c4f807eb68f3c53a5b59df784db8513bcd6ff8cef7e3f0de0e8d4ef/pybase64-1.5.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:20f18edb511ccfb652e114d985a61a4201f9d60bf5a3b3f9e6e95caf3a2f7859", size = 50119, upload-time = "2026-08-08T15:40:06.061Z" }, + { url = "https://files.pythonhosted.org/packages/83/66/5bd414d1dd9aaa1b3c108108f1a9c0d3de6192d8a8753a674c084429a654/pybase64-1.5.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09ba0119df1766bb43ae9774df511b396b89bde68a797119366aca1292f83eac", size = 39873, upload-time = "2026-08-08T15:40:07.232Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/047976dfb83c30be4849bfee783fc45d0e2fdec9115939acf220fc95e9b6/pybase64-1.5.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e3ed723ed56d273b0e3a45c2583c5566ccb39cc5fd4d335bdcbe235f84e1a211", size = 40356, upload-time = "2026-08-08T15:40:08.767Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0a/3927d8d51cfcaf603f0065809a90f31cc5b4f98386eb58f5ccb0fc28bafc/pybase64-1.5.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dd1ace6dacffce5cdbe68a3b2efdf22e3c890a906d887075e10dcc5f4124068b", size = 46956, upload-time = "2026-08-08T15:40:09.917Z" }, + { url = "https://files.pythonhosted.org/packages/56/f7/d9a82433b00952bb64323092913ab1facbdedae097955ddf2b6222686196/pybase64-1.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e8d559a46759687accc1780fbb07be17f663746842853c88115cbf89c680fb4e", size = 47554, upload-time = "2026-08-08T15:40:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/57/08/116876cc8c371a2e10a1a2d870cae935195eba8355b220272798e2897186/pybase64-1.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:b51c308c5732bf4fe5ff6edfd4bced2a32bf41fe664cafc3088d3cff7566734b", size = 40779, upload-time = "2026-08-08T15:40:12.239Z" }, + { url = "https://files.pythonhosted.org/packages/42/fd/40e2339ac17c4b877c9588867144ca15eaab4644e3f59e494b831d73a770/pybase64-1.5.0-cp315-cp315-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:40399e568324635235697b00410634e0fb027432e9b9fef92886eb3407a5c211", size = 90912, upload-time = "2026-08-08T15:40:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/82/e6/5eea2e16c31af2f5c31a7df903df05fb94e146b80709252b0e4da3a09cd9/pybase64-1.5.0-cp315-cp315-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92dbad4599d5d081f905bba43b10690cc4d445857d04a7b18eba1a09bfa27cf6", size = 95172, upload-time = "2026-08-08T15:40:14.991Z" }, + { url = "https://files.pythonhosted.org/packages/72/45/502b486cd801297984f302558973364669b97d1a50e9867e12afa30b5d86/pybase64-1.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1e571d2db1c515641e9918cf04f23be58818ba6d56f266fab31dfc6d5f6e01d9", size = 84916, upload-time = "2026-08-08T15:40:16.276Z" }, + { url = "https://files.pythonhosted.org/packages/7a/00/2c9100bf1f651cf3a6a2c835306603323e3d05c9d2fe7d8ee3727bb7a718/pybase64-1.5.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1e3f5f726bedde8d7006c4f8d61f0f053de65b806af24110278c530445b6da50", size = 88363, upload-time = "2026-08-08T15:40:17.636Z" }, + { url = "https://files.pythonhosted.org/packages/40/9e/fb4520a0cd238065141a89f9f2b6c54ef4e9ff6578ffba6122b5f9af24b1/pybase64-1.5.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:107129bf5591f040cd6cfe3b7ea5c1626a2f9610763e54d450778c578ca2b69a", size = 84714, upload-time = "2026-08-08T15:40:18.809Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6b/87fd652e2c63516dfae373b9563329af2c5baf67a29a499601861cf52d89/pybase64-1.5.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:e161a4ba46caaa9417d5cd55f23c0717d5243b4f2a96c176b0d1a07bf86e0b0c", size = 83687, upload-time = "2026-08-08T15:40:20.192Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/b083e2092dd1b6a6a973ced22b3363dc0bb27e7e2b21da8d83b44097d523/pybase64-1.5.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:741f944bef8dd709e9ca9e991f5f6a91a8d49b6e2725fdb4070027f0ec06faa2", size = 88276, upload-time = "2026-08-08T15:40:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/11/ff/498c12d1316594d3796795e66301e193afa4e51fdd7e378c087922bfb074/pybase64-1.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4c94d6b104411d33df813b1defa8a1194a884e9393839fefa3f7ea7377e1efeb", size = 83045, upload-time = "2026-08-08T15:40:22.974Z" }, + { url = "https://files.pythonhosted.org/packages/da/03/755d6316c7cfdab904311900aafbbbf1ed2227ae814bd3d2f25df8d10d46/pybase64-1.5.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:0976e9b7465387038868c6b560d7cdcbb9ef5214faf55ae6036e4aa4e93ba423", size = 77577, upload-time = "2026-08-08T15:40:24.242Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c5/ebd26ad4032442d6fa6ba14ed0a222bd5d81f2a373d4d83a840a432e6c15/pybase64-1.5.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6fa782fc5d7d53bb4c1b01e34909287f301c4c81251f8130e55848ab5d2f23e1", size = 90946, upload-time = "2026-08-08T15:40:25.462Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5a/46f96588a494ab8ab71a3fae5a18627c728c0379ae96af96312e54f8c8e6/pybase64-1.5.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d3e26e250aa51813881d03c09995a41462e115ab9c3c2b6d5202e4286b924d00", size = 82163, upload-time = "2026-08-08T15:40:26.745Z" }, + { url = "https://files.pythonhosted.org/packages/21/29/263bb998064c5d18957f5445a381f336070d24f09e7c372a0e3963dea142/pybase64-1.5.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:f4135c1e12615fa7989c9aec4720cedaa342bc4b8dbd5665f84a95790e3db5fd", size = 87170, upload-time = "2026-08-08T15:40:27.958Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/e5b3e991ebcc71d20ae9246011dde389e321f450b658976be7a51ca50824/pybase64-1.5.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:6ae263c1244bf375420fcdfd5ab32d463496f3814177edc8f0f3a8b56d7fe643", size = 81041, upload-time = "2026-08-08T15:40:29.641Z" }, + { url = "https://files.pythonhosted.org/packages/6b/37/a6e17849a37cb94b010b4eb7decaa5b49b6fafd0d18b386bd1cbe4b4d523/pybase64-1.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d0930504fbe5c003f31d67aeab4b8f155a409168a26ef8ea7df759bc50ab6729", size = 93974, upload-time = "2026-08-08T15:40:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/9d9f38918c9b27ac7200302fde0baecb95daf3b8a3cbc917238291691134/pybase64-1.5.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:9edbf7e7a97454904a4ccfbd007a511b75ebf13cba9d0dbdfe6c4480e154edf6", size = 33157, upload-time = "2026-08-08T15:40:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/8b/61/aa5c8775a93b2833b016c620dea0debcde2169e6a377008c7d0d34e0d640/pybase64-1.5.0-cp315-cp315-win32.whl", hash = "sha256:eed1b552f5979a4e3545dbaed4dd8111af9d321844232945bd0ed3a505602dd0", size = 42700, upload-time = "2026-08-08T15:40:33.408Z" }, + { url = "https://files.pythonhosted.org/packages/ba/26/b0e1f7dac48ad2b57652dfd3efdef9d0d3f251184802bebc584c6a3a4014/pybase64-1.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d5a27a14899cb1b878c2924dd150d943c4e5cee02a50a409a1f62f4ad852038e", size = 44864, upload-time = "2026-08-08T15:40:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/51/bb/a9e218f092eabf20e893f3490b1fe334c410c0c5851a2a87cfe7157ef3df/pybase64-1.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:163586e9ec367158b0f744ae12d27a28381f85dce7b90a4f9aaa901b1fa06d74", size = 40139, upload-time = "2026-08-08T15:40:35.939Z" }, + { url = "https://files.pythonhosted.org/packages/40/b2/d89caf52c642eceda40c074c4d881cb68b560bf379ce90c44821d79df64f/pybase64-1.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:dc96f63170b2fc943ac83da1015c6333cbaf251d12174b6e506315b941dd16b5", size = 47848, upload-time = "2026-08-08T15:40:37.158Z" }, + { url = "https://files.pythonhosted.org/packages/b8/29/baa2a610ac72c560c29965c0c5b937af0c5ac48055ce3c3afaf1ab329b6d/pybase64-1.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0eb9489fe31db090f95affe81fea96c3dab51c24593ce14fef936ce92d802b63", size = 41197, upload-time = "2026-08-08T15:40:38.345Z" }, + { url = "https://files.pythonhosted.org/packages/3f/da/1cdd664628bef3b6108fac20e2a11df8992e1b0d5a1ff1336256d8817961/pybase64-1.5.0-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f8dcf39b6aabed5d3820188451e98d651a9fde2453a2e99fb386941d4bd518d9", size = 97813, upload-time = "2026-08-08T15:40:39.505Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/430bd2afbd179a278c87e282062db898735a9dc17255b223f1c0d4276b5f/pybase64-1.5.0-cp315-cp315t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ee57900cb5d35a79d992800103180d715b68d8b56658b445a10f97e8805982", size = 102425, upload-time = "2026-08-08T15:40:40.745Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a7/6a80063f72ba5bf09bc43a26ad5bb6152a1fc52fec75f7b24d40ec25c37c/pybase64-1.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5d1c9d46d6b8459f5dac87b1778950ad28e27a83d1cdba1d2c34a031dcd57e2", size = 93851, upload-time = "2026-08-08T15:40:41.957Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/dfc900a5a724452defb33dff71a869638e4e58497dc7fe20602d6e650b64/pybase64-1.5.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:84e619e315fdaf8b70d54cdd0be12c7895dcdcd0212a42a67576b33f7af111dd", size = 92634, upload-time = "2026-08-08T15:40:43.164Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/da8aeb775098fd53d55c289089e6fc94b37751d156e130d11f8c137caf8e/pybase64-1.5.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:80eb2c568f1f09283ad7528407a97e84935f23851943ed27206b52664b8010f0", size = 90805, upload-time = "2026-08-08T15:40:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/28a50e85bab801e642762f71d852ae765970a0df8b9915848e822b73d64a/pybase64-1.5.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:69a2c6eaaa3b7e157ddd1c3803d09e5fa80d9aeb5191b81ad60e182662c2a324", size = 89140, upload-time = "2026-08-08T15:40:45.599Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/17c43a329d20a299b25a01a425dfd5f671274a5ad65754ca314720ca9f24/pybase64-1.5.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:e1df96c88f8e9f57cbe25f0d8f28411e2d1cc42be26e99078f6e4efa876dcb96", size = 92235, upload-time = "2026-08-08T15:40:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/11/97/e42e428a4da55f56d873afc555ff18a91e2932a6a44b4367a9c072d09c03/pybase64-1.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:16201c0998c80f0bac0817a792969b7e1f4169014a8a6b32019e005384734805", size = 91740, upload-time = "2026-08-08T15:40:48.108Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f5/4a527a34c2742009376dba884e84b0e34e44253f5e6b951c66494dff488d/pybase64-1.5.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:f5d28afc34ee925f0beb376d2e3ace38267e700994481511686f2b467f11f51c", size = 85993, upload-time = "2026-08-08T15:40:49.352Z" }, + { url = "https://files.pythonhosted.org/packages/43/9b/50bda3bd73f0f20e83a7941b98e5c655ba1cb6d0d9228c192e3b2ee7ea56/pybase64-1.5.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:dc719c38087e09788d40216ebaacc89504dd8e964c0457085a4c1b83695eaa5b", size = 97768, upload-time = "2026-08-08T15:40:50.593Z" }, + { url = "https://files.pythonhosted.org/packages/11/87/befa9e85b22f32b8eadbdc1145f61ebb16d571923954ac258ddb7f96958f/pybase64-1.5.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:7b809817bf0413bcc00cab69d6a055e1fb2626b22359772c2c3570ac3fef7462", size = 87872, upload-time = "2026-08-08T15:40:51.91Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b0/5b213e770f5e8f3df9a09d4337821a7ffd5001e56a248ebf782a6a8bbce7/pybase64-1.5.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:e953b14d562b7c08eae7b7c327b5162c78a6975974d8de8d7acff2b8b7c682b0", size = 92278, upload-time = "2026-08-08T15:40:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/75/58/1751447e2f3d480dad36d9d0f4a18a65062551ada9cf5a18599a79583536/pybase64-1.5.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:8a5aaa4343b5ed1af3850ce351482e7385d695af15b81b244c3f823949dfe796", size = 86299, upload-time = "2026-08-08T15:40:54.516Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/d505ad3dab5ae4339d0ae471453a305ea7cbe9be630825fb06019d18fe0d/pybase64-1.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a80b502057361c8f2f5f9b75ecda9127b4ea1b1baec7b99b63d425c09e799b12", size = 101313, upload-time = "2026-08-08T15:40:55.813Z" }, + { url = "https://files.pythonhosted.org/packages/ee/12/dfdea7f9d67339a32a5fd85e522b4d1d52f6200dbd29c4cdf190c0802f16/pybase64-1.5.0-cp315-cp315t-win32.whl", hash = "sha256:925f34f75e024abe94dd0f33da8f0cb21db35f85d534219dc18abde90c06a8d7", size = 43070, upload-time = "2026-08-08T15:40:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/29/2d/517ea25a383585fba3b0c5d86a68b72782f45de7dce55c4324fe6c9bb69c/pybase64-1.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:15b0ac4dc01be9a7d2a3e508720a8e3aea9f0dfb1a3dd62b7d5a23f35e76ee7d", size = 45399, upload-time = "2026-08-08T15:40:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d3/9a61bd2e91a45e51806da7e3ab8c6957ba5d83f4f16e56af32a98c14a97c/pybase64-1.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:ee074ecc63f43c664a35c9aea9daa84ab9d0de24487353f53aed097012c8d43c", size = 40432, upload-time = "2026-08-08T15:40:59.787Z" }, + { url = "https://files.pythonhosted.org/packages/e4/99/9cc7eadd3dcc3b9d814a15381fe78bc59dff133d25ba3a8e49e4380fff30/pybase64-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:a9bcbdefd0858372c2e3c657ca8c1e2cdf0af5963cb45085cc861dfac0ddd422", size = 48565, upload-time = "2026-08-08T15:41:27.275Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/0b073d5fe8d035c3334d44252218e82ca0717f71a1139efdbc1600c38463/pybase64-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:8b47a5b4a359e42b4b726cbd9558347c5324194aadaf12e4ad219efc89dc9812", size = 43122, upload-time = "2026-08-08T15:41:28.596Z" }, + { url = "https://files.pythonhosted.org/packages/ba/dc/cd57bd8629965d69eaaa721cf915f3c0590ba468811d290bbcdd3908f0ee/pybase64-1.5.0-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b618ecec8f13b3f9dd58e257aa98fc9b017829a1bdc4f576e9146998956ec2c7", size = 54270, upload-time = "2026-08-08T15:41:29.872Z" }, + { url = "https://files.pythonhosted.org/packages/aa/22/67ad2ddf8ed03e0fc94341ebfc6ed694a36b9c908dd5a08b3ca366e31892/pybase64-1.5.0-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d09d63b219adfb1b40e104036dc2462234d2f06c05e436918e08f31a09a973b", size = 45919, upload-time = "2026-08-08T15:41:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bb/4d080faff127cc8e5e0f5f6bb94d3a079235f83d0ef7355663f4bf214935/pybase64-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:b059b951347a6e16d29b1488f624a7b213c7e8482869b1eac2b684e6fb1ac236", size = 45025, upload-time = "2026-08-08T15:41:32.601Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c1/a45419d6798db99f722c98af153c070964ba8f65c276cb9f771b1407ed05/pybase64-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:abb4aedb092aca028e1111998a0c2a5b6e327707e61df2c22e061118b0a8ccd5", size = 46862, upload-time = "2026-08-08T15:41:34.432Z" }, + { url = "https://files.pythonhosted.org/packages/87/8f/1a25e4067c972560e7f509c814020e946201288d546dbf1882b445b77094/pybase64-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dedecea1ef347db51736836fb609168ab376cdb956a5ded576f271054fba0efc", size = 40205, upload-time = "2026-08-08T15:41:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/d5/27/26473f003bdbe6d8fe79904ae23ab99e5ccb8ab4e28e147495f5688358aa/pybase64-1.5.0-pp310-pypy310_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1439d84162a4ee5598ff324b63f651d9c5adaaa9fce271764384cd55f50bcf2f", size = 50290, upload-time = "2026-08-08T15:41:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/8d/51/0ca9973a6eff45ad7351dac69db42fce4e07b18e2b90e3274800cfcf6262/pybase64-1.5.0-pp310-pypy310_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6a005b8dcc724f0dae96d0504f93d16a283d9a79bdaeee57648335ff0b483470", size = 50765, upload-time = "2026-08-08T15:41:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/f4/05/994876865682591276e95cf19affa229a1026efa9f4c352911ad0bac807e/pybase64-1.5.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:14e4eb6091afd1cf956a37a331566c453aa080fd692acfa76f35761a04f19fd0", size = 45234, upload-time = "2026-08-08T15:41:39.669Z" }, + { url = "https://files.pythonhosted.org/packages/44/f7/e4f8151070be79b13aa276490981abbc283d0a983918cc052d4f02b21a4f/pybase64-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:50eea4c9a05308fbae30ee976150e7416baade27970ae8e229174ce92b5e07bc", size = 44693, upload-time = "2026-08-08T15:41:41.378Z" }, + { url = "https://files.pythonhosted.org/packages/88/84/d011c9b098996db666cb971a831c715d693209e39d28690af1b8049ce3fd/pybase64-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89756a61cd09a5669ce923081f518476ff4b960c5d850a5dd54f0cf4406ac684", size = 46808, upload-time = "2026-08-08T15:41:42.904Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/8d6c3aea8fc68875a9d1b4fa750911a2e2e019f984498f7a21807fd0cbed/pybase64-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0e8691ad425ab8c586ad93a2d71789c6ab86e201377619ea146ab0ed092aa2ab", size = 40032, upload-time = "2026-08-08T15:41:44.26Z" }, + { url = "https://files.pythonhosted.org/packages/86/d9/399c45ada7e401c927345324baf797b167864b9817be6aa71a1e28a00ad1/pybase64-1.5.0-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:adfc52cee3ad56c070e824bee9feda1f13c8679601ff8d0535f03da60bdcdda6", size = 50312, upload-time = "2026-08-08T15:41:45.607Z" }, + { url = "https://files.pythonhosted.org/packages/62/18/064288c6211c79a27893b70261558cf79a254ca22d80101bd7d05a817a6f/pybase64-1.5.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d240554e1a63ad9b7cb128acf94d4bc7d8400c78dfb76521775e767d4aa0b22", size = 50782, upload-time = "2026-08-08T15:41:46.855Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/d68d5df0f367613643c9cb9470a25611d2d078471c1eadeb86c00e644182/pybase64-1.5.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b7af9ad847b351b42ec54b3c0580febe406b28408917b7fc1565c87896ed0c4d", size = 45251, upload-time = "2026-08-08T15:41:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/1dc6e9d781a00fc427518ba462d6ed5b15caaabe3aa74aaca24b2b68ad26/pybase64-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:83496c6800d5e1002d1e923ab5bef49bb67a07c2faac8374364497182f04af72", size = 44720, upload-time = "2026-08-08T15:41:49.384Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "2.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "genai-prices" }, + { name = "griffelib" }, + { name = "httpx2" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/ba/27fc9f4b2ecf0db16f9f8b79a07b0f7a0770e2e91b6a488821d868908895/pydantic_ai_slim-2.32.0.tar.gz", hash = "sha256:2b3faa16cd183f6797524e472d7749c29c2bb9161e05c5bc15e5d5e60d1770af", size = 1225008, upload-time = "2026-08-19T04:02:53.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/78/1d996f58f415f7ed032992b4c003def59c4c9200947cdfe41abec43fd969/pydantic_ai_slim-2.32.0-py3-none-any.whl", hash = "sha256:4bf57077c6b928fa1ab44069736a853d1d8754652ea25060e32b03fd6bcf6269", size = 1443791, upload-time = "2026-08-19T04:02:46.842Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "2.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/4d/17f1b887563f1082f286df4e89e95e44b3963fb730d1973e08ba8d9d4fa6/pydantic_graph-2.32.0.tar.gz", hash = "sha256:d3a2189105af1734bb2245fc12db0957cab9fd3f2fd7af9bfbeb1e02b79bf640", size = 45163, upload-time = "2026-08-19T04:02:55.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/bd/2d32bbfd4abf3682ef1fb5b99265a70bff1b1f87da53e7b8f8e351e3c4b1/pydantic_graph-2.32.0-py3-none-any.whl", hash = "sha256:db85d62de6126aa4a7b61f04caa7c07f76ac0f5fb36b5b70db6abcd955aeeaf1", size = 52648, upload-time = "2026-08-19T04:02:49.665Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pypdfium2" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/78/a52cb80611339ec95f35c7a10d7bfe7a6f97f3b50a35a9f94283d062512e/pypdfium2-5.13.0.tar.gz", hash = "sha256:7ca2d8e31bd8d0d40c496416b7d8bea423388669ffd494929f50e8c3a82326b8", size = 273639, upload-time = "2026-08-13T10:58:15.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/9c/a49050af85055054299c7fab658ac63f8fddde575774aecbf8f71c7a9e5f/pypdfium2-5.13.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:882f4bbd4b17a335b43603169a14cde9341de12b238acd5c39e690cbca7c4293", size = 3417299, upload-time = "2026-08-13T10:57:40.522Z" }, + { url = "https://files.pythonhosted.org/packages/50/ad/f23027328843ee2bdd05afe16bb101f5906befd0c70de35fa8c53f60a5ff/pypdfium2-5.13.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:d96929bde3bd64c771ab3558ca1ffd7704cc4d872ab92cd9f8f8b8a20f7f36b8", size = 2864708, upload-time = "2026-08-13T10:57:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/08/99/1fe58428b69d2722dcbcfaa08ce71834a332c5b518fd58874bcef936b823/pypdfium2-5.13.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:da5c7b74eebf40b5c1fbe1de01aa1edc8827a79fb1efd999616bc20dcaf77ba4", size = 3507415, upload-time = "2026-08-13T10:57:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/06e26da88a4f5b4ed289325868717a186020661b7b221aa6df622711d31b/pypdfium2-5.13.0-py3-none-macosx_13_0_x86_64.whl", hash = "sha256:2abedfb5c70992b19c780ed58d7f7b929e8ce8ee52c9140158f44317c90ec6c7", size = 3670979, upload-time = "2026-08-13T10:57:45.607Z" }, + { url = "https://files.pythonhosted.org/packages/fe/31/f8210d53775f142be934336665b1d60e800c3f176f28c29b4908d945c518/pypdfium2-5.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ee8c2bb2e68b396ab4a763215ac100dacb6b96d0da5bebeb239a021aecc3a7e", size = 3676486, upload-time = "2026-08-13T10:57:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/94/50/d339fa09fbe592564b100bfc76833170a1104a764a458ac2abfffcb632f2/pypdfium2-5.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f58e91b8c45ca144a1ff3008faf3c73ef8a5e9fb32988831788363288228cd", size = 3400883, upload-time = "2026-08-13T10:57:49.189Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e0/b10cf41b5e9f0212d014c40635659c6ab95bb4fcc6fc47f5d3c571f8d57f/pypdfium2-5.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46b2f5be9e7ae941ee4216e3d20b66f9dc3d81944a3d57756272de5275204709", size = 3803912, upload-time = "2026-08-13T10:57:50.865Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d8/25ba4ce9a9059ece82f4514df0658fde0aa9bbeafe135e76017c052bf56f/pypdfium2-5.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d96beb7f379e6c76d874ca93fcd182ac3168dd499056407070f9927fb1061b8e", size = 4218231, upload-time = "2026-08-13T10:57:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7c/74a2fb48e5b0d2402d9ca64b39074c722d67e9a8a2c58449a843a8c2329a/pypdfium2-5.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81df25c1ab4c13ff773102d3cbea1967511d079123b067fc077bd0c4d57d91d8", size = 3730077, upload-time = "2026-08-13T10:57:54.021Z" }, + { url = "https://files.pythonhosted.org/packages/59/12/8c922f00518c26dc47d3676cc09c1d3c95e991c1977e31067d23cc2215cb/pypdfium2-5.13.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a32d89fa5b4a2715810171239eb194df4aba604727483ab760512f3c6a851", size = 4031512, upload-time = "2026-08-13T10:57:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/c6/48/a171d034c2dac01adcc57d3dad3c97ba11f19d916f421176002c9e02c904/pypdfium2-5.13.0-py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b90b0a5ac310bb34db8eb848e58fcab4e201e124e3cf3cb1ccb7b85293e034af", size = 3995485, upload-time = "2026-08-13T10:57:57.39Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/dcb24776d409bb9e5b7fb26a0c62a87b98ab0e30dfcca645eaf31e35123b/pypdfium2-5.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ada81c36483cd61d07e32bc7814620ee96256b4f421b913f566861bf91800248", size = 5016636, upload-time = "2026-08-13T10:57:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/93/24/1fab8470fc6de6f4481f009c90757b1a1ee0a61d8e864ed273f72ffca855/pypdfium2-5.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3826e521e895648983cb9ee6b934d4bf51552600043984f84e9c2b3b14b696f3", size = 4555251, upload-time = "2026-08-13T10:58:00.753Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ef/6e8dbea1eddcb55cf34172753ffccd39566333c803cc94d43c653f369f2f/pypdfium2-5.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5c029d7163a91f264eafab51fb442a84a33efd9fd83d5a06c0136a7857a3cc8d", size = 5263483, upload-time = "2026-08-13T10:58:02.48Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/2ff673730189a621c01f9193c74b0f6aa70d8740889fdf11949e1c541869/pypdfium2-5.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:be2dccbde0ce7efe334ecd8f348df4308db360756ede4f0821d82dfc9a58caa8", size = 5144135, upload-time = "2026-08-13T10:58:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/19/0b/759b9037c007317fa5c990dd3f6eff2b99d3fbced251d1e2512be92f2e2e/pypdfium2-5.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:bcd81394fe101405e026eedb3e40bef84635c1e5d974dd6036420eb6937753c6", size = 4648156, upload-time = "2026-08-13T10:58:06.036Z" }, + { url = "https://files.pythonhosted.org/packages/db/3b/ffe29679c52efe8eb02d77aa6656e6d6201395423329af018ebd5923a3d0/pypdfium2-5.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:2ed32ff685f8e05e637c990bedbf5fca66727bf27718d8bc33eeab21ce0630d1", size = 5089852, upload-time = "2026-08-13T10:58:07.791Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b6/cebacc1601ddfdcd1e6a1dc321533d215ceccf9b825fa9b91b11c6dc39fb/pypdfium2-5.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9c777edba28d1d5fd15435ed3a78ee2fdb93dd069be37cb53b559bc122793770", size = 5074153, upload-time = "2026-08-13T10:58:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/54/40/cf14c4f534f817788966857afdedb90002198dca5ce4fe2c6ecb031955ae/pypdfium2-5.13.0-py3-none-win32.whl", hash = "sha256:d33ee7077db67478b75efe4b5ea9610fb96c5416a0bc4949227f0f59c34dfcd9", size = 3753164, upload-time = "2026-08-13T10:58:10.97Z" }, + { url = "https://files.pythonhosted.org/packages/5d/99/a37b6b902457569468ed5908c94e56cb6c4032541f02cf89f723d42a9148/pypdfium2-5.13.0-py3-none-win_amd64.whl", hash = "sha256:47dcca2a8d507b5fd24f94c3c9d48fb379430f097bc20f01beff6c963ffbcedb", size = 3885553, upload-time = "2026-08-13T10:58:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/50/7f/d39f6e64375c2ffd50ea100e3c73af79085c880c2791eb7203bc61d8913f/pypdfium2-5.13.0-py3-none-win_arm64.whl", hash = "sha256:554a0b23376460af1410e3c915906895e2dac67a086b9e6ccde0643a795d3b0d", size = 3700026, upload-time = "2026-08-13T10:58:14.206Z" }, +] + +[[package]] +name = "pypika" +version = "0.51.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/78/cbaebba88e05e2dcda13ca203131b38d3640219f20ebb49676d26714861b/pypika-0.51.1.tar.gz", hash = "sha256:c30c7c1048fbf056fd3920c5a2b88b0c29dd190a9b2bee971fd17e4abe4d0ebe", size = 80919, upload-time = "2026-02-04T11:27:48.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/83/c77dfeed04022e8930b08eedca2b6e5efed256ab3321396fde90066efb65/pypika-0.51.1-py2.py3-none-any.whl", hash = "sha256:77985b4d7ce71b9905255bf12468cf598349e98837c037541cfc240e528aec46", size = 60585, upload-time = "2026-02-04T11:27:46.251Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, + { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, +] [[package]] -name = "colorama" -version = "0.4.6" +name = "requests" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] -name = "exceptiongroup" +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rich" +version = "14.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +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 = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[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 = "sqlalchemy" +version = "2.0.52" +source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/a6/d7/e0354e7334d33ea2795db3ecbe2977026c05a1ecf8ba4b5953c329872453/sqlalchemy-2.0.52-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7438774e1091192fc50a2bd8ceff5c596912d00ecd46587e88effdea7826101", size = 2172616, upload-time = "2026-08-11T20:58:21.078Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/98eef682e6946eb1b4195a9a2393db4662ebfcc89f823ae78b938765c3c0/sqlalchemy-2.0.52-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c1b7ed45bf87b214e0a9def9c2313949067efe6269db5ef18d542ee13250af7", size = 3279798, upload-time = "2026-08-11T21:00:03.535Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/5b96afc1407c314347ad006b72bb251fb68ef84d05505ebf8a39bc47fcde/sqlalchemy-2.0.52-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309cc8ba50fc5d2174189dfcd49cdf7aa711f8346afcff19f2642ae4fc449c14", size = 3277555, upload-time = "2026-08-11T21:05:47.932Z" }, + { url = "https://files.pythonhosted.org/packages/50/69/ce6776724511d1b5dd40477b08d6a5f0953a45375e092dfa852b1857732c/sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2f9eccf8793c8c3f8dd2dfd11b9e400cb27d1d19370ef732b66017e212107822", size = 3231246, upload-time = "2026-08-11T21:00:05.184Z" }, + { url = "https://files.pythonhosted.org/packages/72/19/ab0cb9ccdafa2419c796ae62f8740aedb903f1e93bb326064b1a0147e458/sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9255ceb65a80c1b001129060b63ee776a2e9c288be3b662be36dfbb888fffdcd", size = 3250763, upload-time = "2026-08-11T21:05:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/f2/3c9b54b61bec4f493c0007dc9e2700c963c5b32667e21a506f1aca7a8115/sqlalchemy-2.0.52-cp310-cp310-win32.whl", hash = "sha256:2e15b1d1116a64fc399b8c2694a83f3e792fdc58df28514a81e1dc4f8cf22729", size = 2132169, upload-time = "2026-08-11T21:09:48.108Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a1/934bb6cf543a398c72784d1fc777eb530559c16ebe1549fa7611e5989ce9/sqlalchemy-2.0.52-cp310-cp310-win_amd64.whl", hash = "sha256:11560064cc4696e772298b6221ede59e646386d9f2a85d549365473b972f7850", size = 2156289, upload-time = "2026-08-11T21:09:49.429Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/cc5f7627b92f1456bc0b5fb7e98af4600248abe422a44da0d17a3fe6a448/sqlalchemy-2.0.52-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c", size = 2172460, upload-time = "2026-08-11T20:58:22.429Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/9a2abad8bfc8fdcd38c64adc056aeefab7aaa96ecd32f5e8c140e6375f17/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608", size = 3355720, upload-time = "2026-08-11T21:00:06.746Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/e75597b5841043e3c74055d00d4feb53d9a49a5c89ba2450d2d9aab53597/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1", size = 3354394, upload-time = "2026-08-11T21:05:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/410fbc6c2f1fa8310f4ef1b6847d47d0ac1c042c7b4e81eaaca063d030a9/sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8438a98d49424acf69d0d53c0a522951dfe49a6f2d86417fbb37ad3066ab43", size = 3306991, upload-time = "2026-08-11T21:00:08.603Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ba/25ffd5c24681ea4b46e62c80ceca8200ce204de1773366321306cf3f608a/sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4699dbb8d396d199e7e78fd4d525e3ad3d6008a9c8c0160b87e74c606c2c3736", size = 3327454, upload-time = "2026-08-11T21:05:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f1/0f1b1d4800e51218e736a06ed55a3b2a59c257600bbaca7673bf13d2dbec/sqlalchemy-2.0.52-cp311-cp311-win32.whl", hash = "sha256:cef328349452ae152637df4d11ce5a0919ecdf0a363e16c830c3518ee33bde72", size = 2131248, upload-time = "2026-08-11T21:09:50.765Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/04d2ac5ad66f3d31278f37064ed5f5ef3fe653f7bdaa67036663f223d186/sqlalchemy-2.0.52-cp311-cp311-win_amd64.whl", hash = "sha256:f1c850792a3b25a3ad74dade3f05e4f402cdebfea27438bcadafaa1617f77bcc", size = 2156943, upload-time = "2026-08-11T21:09:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d5/1b77a026d161f98a08f11af1a5f6c47b98ee7c7e2648af525a1004826c78/sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727", size = 2170940, upload-time = "2026-08-11T20:58:11.25Z" }, + { url = "https://files.pythonhosted.org/packages/54/bd/f444444adb37b5d53753fb1730ee7a421628e2e3b756c4da461af7e6394a/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee", size = 3383415, upload-time = "2026-08-11T21:02:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/be/57/2eadf93a552568c57e8680b7e58bb5e9770d80942a1bdbaf4f2f63f0d7c8/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b", size = 3398577, upload-time = "2026-08-11T21:16:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/15/c3/2887cf9dd111d1fbf05d22165b404c221ef43e029f7a2695e7302f27a7cc/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee", size = 3328225, upload-time = "2026-08-11T21:02:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/02/0f/466bdf9e1feeeef5587f868c187d8687e21ff8c85b1775e9041130181132/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf", size = 3357374, upload-time = "2026-08-11T21:17:01.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/5c2b4583904af4173076dda1c9e53c9e2ffc7a702d2efde0216bbacbf7cb/sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e", size = 2129366, upload-time = "2026-08-11T21:14:50.991Z" }, + { url = "https://files.pythonhosted.org/packages/ed/06/543dab8ef62d4e9fb96fb31a30c2b8b14a8763bccf48d428294d6b3041c0/sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca", size = 2157344, upload-time = "2026-08-11T21:14:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" }, + { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" }, + { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, ] [[package]] -name = "failproofai-sdk" -source = { editable = "." } +name = "sse-starlette" +version = "3.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, +] -[package.optional-dependencies] -dev = [ - { name = "pytest" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, ] -[package.metadata] -requires-dist = [ - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7" }, - { name = "tomli", marker = "python_full_version < '3.11' and extra == 'dev'", specifier = ">=2" }, +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] -provides-extras = ["dev"] [[package]] -name = "iniconfig" +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 = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/62/167a842aa0429d45f5e797354fd4343a96f6043d67d0513c675c7b8d36e6/tiktoken-0.14.0.tar.gz", hash = "sha256:231dec90efcdccf1b565a1416107736f1e09b1a08fe736ef9d6363e626d03874", size = 38898, upload-time = "2026-08-17T19:49:49.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/82/d60a7a5d7bff7b4641d556ea68ea5914ea6edc3774a12eb1c0d444701382/tiktoken-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3b12e54f8bec91433e41aff65d8d1f209a4f678081163747079806e5361f6c91", size = 1095817, upload-time = "2026-08-17T19:48:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/18/e2/d39ae33d3dc30a0c229ff0cb683df961ebb5e7b8691feb2d08b3ee6ac327/tiktoken-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:94f77b60a8ab23580db19ae822744c9716c1720020d2179ca5605112d12326f1", size = 1043064, upload-time = "2026-08-17T19:48:33.138Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e9/8e18cbee0c3ae8321c7e9696bef6090a24eed99a4a75a4c4a7f5115e5a2f/tiktoken-0.14.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:f3d6cf93fbe2e7117eb7bedca684216fbe328a41f0843ce34245451d8eb2df1c", size = 1190381, upload-time = "2026-08-17T19:48:34.386Z" }, + { url = "https://files.pythonhosted.org/packages/af/c8/051e7b72a816ff50eb34a1c7c5b185cd2429ffdf59a497baea35b2b6b2dd/tiktoken-0.14.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:18a1b651c4b032004bf7b4f1713391a54b2a341a52c6e8a2b59acae9d16e13c7", size = 1206869, upload-time = "2026-08-17T19:48:35.581Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b3/7795db206adb6a57d6137fe48ef2cca6b9707e90b86ee8244671592ddc33/tiktoken-0.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d91d68353bd167fdf26467e5ff9e56aaa5f87d6410c0238608629e4dc0d33", size = 1255197, upload-time = "2026-08-17T19:48:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/c8/39/5234783af6b81af645ccdf9438f2f02af472f14e91d876ca2079af641841/tiktoken-0.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10f31e63e40313f2e518d87f7086cfa44e45f64cc14d8ae14103b41220c30a14", size = 1319329, upload-time = "2026-08-17T19:48:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/88/cf/f2d955c8c5c6c67cc86ba6fb132c47c710465ebe6a6dcec1c3b6e250660e/tiktoken-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:c6cb9896a82b9ee44e15ba0b5c8044072f2e4d48acaa704c8d3feeef5ad9487c", size = 944146, upload-time = "2026-08-17T19:48:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c5/9d848b7f408241171e1f843deb8bfa626086452bc9c78beee500829583e3/tiktoken-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c2edf09b381fafbc014ae8e018ed25087abb9a3dafa8465a0ea63c6558c47a79", size = 1094971, upload-time = "2026-08-17T19:48:40.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a9/d94302340304328961d6f0c35ca4e60617fbb57a5cf667e2ed1692cb9e57/tiktoken-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cd8ca1305c1c902fe42c486165f2e4808d9997625c98ffb05b9e0366d99d3948", size = 1042916, upload-time = "2026-08-17T19:48:41.541Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b6/31da98ee871383509cae2ba96a9ddef1965e3c4f8cb6dc7bcda3379398db/tiktoken-0.14.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1f83081065ee5833d35b49e9180f3d8d15622a603dd1c435da0da6cc12b3662f", size = 1188650, upload-time = "2026-08-17T19:48:42.729Z" }, + { url = "https://files.pythonhosted.org/packages/24/65/8c5dddd7cb67f6571d154a58d7c6e2f07da54bf84c49b6a1839965b7c35e/tiktoken-0.14.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7665f6624e052e5e7f6a36919ab69279decdc976d7b16b4fa15e1897d0513", size = 1206378, upload-time = "2026-08-17T19:48:44.013Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/522ec59d30dd9a2f3ab837011cd4fc5d1178dc4a2fa07c9fa4b90af6ba9d/tiktoken-0.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:144a3fc369f92b7d548995217c5d6e84038d3572157a0f6f34080d65291d0f78", size = 1253694, upload-time = "2026-08-17T19:48:45.597Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/9019e272bad188a1c61ecf44f25a9ba2368744644e3ac1f3d6516f3c9e80/tiktoken-0.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:151d37a150c8f3dfc5f4345597b10e101876bd1bd13494e0185af6b508758d2e", size = 1317873, upload-time = "2026-08-17T19:48:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/fff1217240343c0c11b5938b98aeae0e3a266cacfac25f86f91cdcd748f0/tiktoken-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:c77d4a3e1deb2707819df92046b89aad1ac81d27e07616b797cbff3f62c037da", size = 944395, upload-time = "2026-08-17T19:48:48.028Z" }, + { url = "https://files.pythonhosted.org/packages/8c/da/e273746b9d24a63c776bc60fba914351573ad9c575b52601eb5e60632564/tiktoken-0.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8e947aefe98ef74cce94923f90e48c98fe34eb1ec0a6bfdfadfc5a96359bfc36", size = 1094408, upload-time = "2026-08-17T19:48:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/69/9f/fe6b1aca23331aa5271df5a4bd07bf68a7059254d47faee1b8272592a777/tiktoken-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cebe67765569df3dafac8474e4eccf5c19d24140492567a5e58a11445732a4", size = 1038499, upload-time = "2026-08-17T19:48:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/0b/35/e9f47647c9e163bd1de30fe1a491669b7248cfc67b7404c35c009a701e1a/tiktoken-0.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7db45b98e94adf4173a5cd7422b150999a7ee11ff847783a14f6e1b80cc38cb6", size = 1186355, upload-time = "2026-08-17T19:48:51.93Z" }, + { url = "https://files.pythonhosted.org/packages/51/11/9976ad86980a00cdef05e730a0127a2578a1bc6d11644d8d47246de2eb26/tiktoken-0.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7896eea257fe497a2b7134474d909156c6744ce8da35bce88011a960e008aa0d", size = 1204197, upload-time = "2026-08-17T19:48:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9c/7035b0bcfaa68d1ee4803fc5be5214ad865669b05bd20e7105ae8a18afc6/tiktoken-0.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b950248272f1b303dc32986396e2dccfa10cf6d1e83ec8f0bba1776660305482", size = 1250635, upload-time = "2026-08-17T19:48:54.392Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/69cabf18bed7f4366da076735816abce0d4db3fae491ae338a6612128777/tiktoken-0.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3de75343041a1c57333b1e707ac8a9769738241d7d6a55d39e12cf84548337c6", size = 1316085, upload-time = "2026-08-17T19:48:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/bd/bd/a2e884fb1402cba5be08836590320012b2d8ada0e2eef9911a64df4bcd2d/tiktoken-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:087538c080e5ff421abd3a0785ed63c5111d06af98e6cd0d374dbe5969147ca3", size = 941208, upload-time = "2026-08-17T19:48:56.938Z" }, + { url = "https://files.pythonhosted.org/packages/50/53/ee1453623bf65f019328721ccb6587846d2c5b7b82f34e73ca09101f072e/tiktoken-0.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9c5fe393aab56469f04e432ff851216d3def3436cf5f07e442a240164bf500f", size = 1094198, upload-time = "2026-08-17T19:48:57.955Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/6448cfe278c3664ba9ec5b5ac08344341f7dc3d42888476e215a14eda2be/tiktoken-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbe2cc3bba939bcdaf103e03df9d5039d33887080b315624be28ec69059e5f94", size = 1038820, upload-time = "2026-08-17T19:48:59.015Z" }, + { url = "https://files.pythonhosted.org/packages/69/3b/d67eac1bcce9dee3abe23aff5e3ded3116bbebaf67b80a0811c06d3806fc/tiktoken-0.14.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2157f52e4b4d7ac5ecc7457b3716834706e7ef9a46f5144029bfeb7cf71f4e06", size = 1186175, upload-time = "2026-08-17T19:49:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/37/62/cae690d9783146b0f81f564ada0f8f611de68178c0c9c7e1e969f0516b48/tiktoken-0.14.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:26e60f6a956ee171ab728b37b8439905d7ea1db435c30f9822f291e9861c861d", size = 1203884, upload-time = "2026-08-17T19:49:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1e/633e30237b94e383cf814145499079f3bb9cdd4aeafc1bc42e01b0f810a6/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:380873f330b741c4435574f37edb20813d04603ace2d53e0a63560e1fec83010", size = 1250980, upload-time = "2026-08-17T19:49:02.274Z" }, + { url = "https://files.pythonhosted.org/packages/cb/56/4c12f07b812f84206f38d723eb1ebfdd34bad9309b5dbc0bee6bbcff4cbf/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3fd7c14b1cb45b486c39fc9b3443bb341f3e2fc7e6f31247f3435a5836651632", size = 1315434, upload-time = "2026-08-17T19:49:03.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e0/c65603f0c44811def666d3fbf611bf2af3b5e1ef613e06c19411419830b3/tiktoken-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:90a762670c7f968184723769a06ed51f5cf5ce5dcd1e30164f25c72d85c2d1f1", size = 940883, upload-time = "2026-08-17T19:49:04.583Z" }, + { url = "https://files.pythonhosted.org/packages/59/b0/1cf129f4af8fc513931f931023def596b7c4bfc77026513cd9d851da9e88/tiktoken-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e067f4cbcc5d036e8aff7fe7a6b530a8f4de2e4616ad9005a24a1879e24e6450", size = 1096273, upload-time = "2026-08-17T19:49:05.807Z" }, + { url = "https://files.pythonhosted.org/packages/62/85/2ae74575e321148484147e10b53c3b1717c59ebaa9edb4fe18b1f5c055f8/tiktoken-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2af4a336ea56d6c14f27741a0e1d8294a35dd0b038bcf990d232ebb54eb994b", size = 1040269, upload-time = "2026-08-17T19:49:06.943Z" }, + { url = "https://files.pythonhosted.org/packages/89/29/92a1120a12e4bcf2d5464350d1a91b68a433d63ce656bb7f806c27aec09c/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f702e0aeeb6506e57687e881c59e844ebe8f0a6a097ddafe20e3ab25f387be4e", size = 1186101, upload-time = "2026-08-17T19:49:08.102Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7d/144af98dc5ad68108451a82e2f5a17f80e2663f5115058b8dfd215c1ad02/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e3442bbb2f0c588cec876061e37ae67b455b9df9978b003c8fe30e45f2ef5b42", size = 1204457, upload-time = "2026-08-17T19:49:09.28Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1f/be7cb06ab2108f612f3e92e7b76cf391e192db0db37a984616f0cc32aafc/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:979c1524f753b662b0f3cd261b135afe6659cce33caaa7a5ea00dd1756b3055c", size = 1251716, upload-time = "2026-08-17T19:49:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6b/81f158d0f90adb826cd704069c2129a046cb784a2a09861009519fc41cf4/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2cc19ac87b41c9493c9778ff5847f0c8bbcf5bd0ec6b87ce06c1c802adc8a771", size = 1315432, upload-time = "2026-08-17T19:49:11.844Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ec/f5fa35ec13f07279fdcaf3cc9c04bbb154ea591d23978651f2b672593e8a/tiktoken-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:eceeff0c62419bc78d4b6e70a4762a4d25df3ae8f2d5946e3853ce93e7a57098", size = 988046, upload-time = "2026-08-17T19:49:13.282Z" }, + { url = "https://files.pythonhosted.org/packages/68/c9/7756717408d3d0dfea3f046c9466144b28afde39ff69d5808f2475dcd7f5/tiktoken-0.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6eb94895c45f26bb8f5546e5fd8a069efcf6e3f108ea9d5cbe3bf6f7f3983438", size = 1096261, upload-time = "2026-08-17T19:49:14.351Z" }, + { url = "https://files.pythonhosted.org/packages/79/29/46ad8061f57bd9f8b2ea0aa82bf574e0f2aa040b0857a1582adba9957899/tiktoken-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:86951a971c53979ec857bd8c4a32dc227ab0fd33f6c12a3bd62d3fbf5f0bfcaa", size = 1040183, upload-time = "2026-08-17T19:49:15.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7c/3184d17b868456f17b60b1a75f5ec0405618a43aa753336df341d8f11781/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e2eca764c53490f8930dbce329e0769f11108d87d908282a80c5c130e26e7037", size = 1186719, upload-time = "2026-08-17T19:49:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e8/46de4400d5bf859f640feee85bd7e32235f68ddf25db53c63be78e581e3a/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:26cc4b4840fa0e9f4b72ed489883e12f57e00d1021ca794720e3c29a12f0edef", size = 1204660, upload-time = "2026-08-17T19:49:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/af8964c38bc8226dd8950305b7a255fa33345d5572f78af7275a313d28e0/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a", size = 1250932, upload-time = "2026-08-17T19:49:19.28Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4b/323631116fc986d9cc5bbeb2b8223c7c85e61a8bb94ea5ab4951023b149b/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58", size = 1315190, upload-time = "2026-08-17T19:49:20.467Z" }, + { url = "https://files.pythonhosted.org/packages/18/8b/ba48a73729c9270989b36f37ab2ed5525e52690d715097c9fa791aaa5d05/tiktoken-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0", size = 987717, upload-time = "2026-08-17T19:49:21.704Z" }, + { url = "https://files.pythonhosted.org/packages/1d/10/b73b7e319179e0f60b32475f783b044f9cece872c53b6662664e9084b0d0/tiktoken-0.14.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:14b47e3674f2624803a8acc8fb367b7e24fc53055f9df3296482fe9a3a34a232", size = 1096280, upload-time = "2026-08-17T19:49:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6b/09999a9bf1d559670d1680e8f8e419ac0e2c5f6aac82e9bfdf70f260b30a/tiktoken-0.14.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:19d643d701fdaa70e5b9c7f8f96abcaffe77ca5e482a3a1a7dde46feb4284695", size = 1040433, upload-time = "2026-08-17T19:49:23.998Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7b/8537be0836f3df99b2a636b44399bfa43cd757f2b8b4097dacb794cf24a7/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:e4ddf863b59347deaa92302dcd90e5eb003cdc9be06ec2b692c38d1bdd9efd49", size = 1186989, upload-time = "2026-08-17T19:49:25.021Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9d/f9c56d7a943a4468abf9ef37661bb9b8e0cd3aa8aa87368c7146cc3f3222/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:60c47ca69ddda0dea8256fffd12e1b86f4b59734a20e4a70c61f63cc5f021df4", size = 1204615, upload-time = "2026-08-17T19:49:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/98a38579db25c4a8a84e31dd95d9072ec5f21f7e70de591da0412e29b25b/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:728303a072163130c5b477b1f20d6211895569c1d5302c24ffc93a3009160871", size = 1251828, upload-time = "2026-08-17T19:49:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/0c/83/467be424746c039c5493c0f4102feab16b9b48eb6f5c089b2a2438e3cde2/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3c5349c9f916283bba32bec8af69b763e4faa304dc004d0eaaea66a3cf004c1f", size = 1316260, upload-time = "2026-08-17T19:49:29.101Z" }, + { url = "https://files.pythonhosted.org/packages/02/ee/ddf46ca78e371f5890e96b6e7d089a85b3536432be219851eb0481786ca8/tiktoken-0.14.0-cp315-cp315-win_amd64.whl", hash = "sha256:1b6e4adcfd285c44502aed51df98aaaca4f0fea028165dbf8a9e857b9f98d8ea", size = 988230, upload-time = "2026-08-17T19:49:30.246Z" }, + { url = "https://files.pythonhosted.org/packages/2a/00/5162e90c851a28da18ed382d34898b79a8022548e5619a64e14c03ce7c3d/tiktoken-0.14.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:11d8211b290855d2721334ff17dd9b3a17bfb26872be01f25d73612ef7ece890", size = 1096186, upload-time = "2026-08-17T19:49:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/65/97/a5a7bfccf25b1bb65e82bae8edff11ac3c9c041c374b7b4a823d60c38133/tiktoken-0.14.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d0781223705199b289faa59601bb9c2441712d4c600dd13c43d8fd6a33d22cd5", size = 1039947, upload-time = "2026-08-17T19:49:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ba/ef427fc638f1439181c5e12dd26b70e881861f89c007aa7e5b36300f8342/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:2ea70afba6b9eddbf22c165142e5f0a2ad7aa36a452873c48b57bb2aeb8492ae", size = 1186997, upload-time = "2026-08-17T19:49:34.121Z" }, + { url = "https://files.pythonhosted.org/packages/3e/88/2f3f85a968cdc514152129af0a060ebcccb067005a2f29b0d5ef3c838514/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:78571efc311c30b73f31eb949a921d6dac39a5d9dc42d1cfa8f8db157b3447b1", size = 1205211, upload-time = "2026-08-17T19:49:35.284Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f6/80760e98a08e6649d2d68afb6035af713121dfb615acce8c4f73810ec438/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86f66c85e796f5d05d5c4a60ec1d40cbfebc47a32464053528c797163fa9ab89", size = 1251479, upload-time = "2026-08-17T19:49:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/84/50966fb6918a0fb9b32721277e5342bf729a2d74350074d662fbedf9772e/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:149d97453c4c98c04b081d64a85e635921269b532710d6faf81e9e82b790e7d3", size = 1316673, upload-time = "2026-08-17T19:49:37.756Z" }, + { url = "https://files.pythonhosted.org/packages/35/5e/9b01afd037bfa22a0033963fa091e0f75b6fb15cd85bffb42ff86e697323/tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9", size = 987929, upload-time = "2026-08-17T19:49:38.947Z" }, +] + +[[package]] +name = "tinytag" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/a4/a1d39cc10b43cbbae268127a1c38d689bc6a85cf966f9445bc9f1f5f517a/tinytag-2.3.0.tar.gz", hash = "sha256:84850f8045424b944475b9754bc35c7e09bcae1ab08d1f88d9293aa33af39a27", size = 44379, upload-time = "2026-07-30T23:35:03.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/90/48/f9a955e0d27376dd0a6bcf7973818b19e5965dd847580aef2f08c6d6f64b/tinytag-2.3.0-py3-none-any.whl", hash = "sha256:231ba5b2fb7a6db478f6dd344ebf20dfdfcd5907f142d475605d516dd7e8b9a8", size = 37155, upload-time = "2026-07-30T23:35:02.003Z" }, ] [[package]] -name = "packaging" -version = "26.3" +name = "tokenizers" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, ] [[package]] -name = "pluggy" -version = "1.6.0" +name = "tomli" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096, upload-time = "2024-10-02T10:46:13.208Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237, upload-time = "2024-10-02T10:46:11.806Z" }, ] [[package]] -name = "pygments" -version = "2.21.0" +name = "tomli-w" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929, upload-time = "2024-10-08T11:13:29.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440, upload-time = "2024-10-08T11:13:27.897Z" }, ] [[package]] -name = "pytest" -version = "9.1.1" +name = "tqdm" +version = "4.70.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, ] [[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + +[[package]] +name = "typer" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } +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]] @@ -156,3 +5074,932 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3 wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +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 = "uv" +version = "0.11.33" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/2b/0820fbaaeeee0296e93d43c7b53e5dfa6118bda890ca9b7ea9d679b15065/uv-0.11.33.tar.gz", hash = "sha256:a4411bf854f5fe3d4b78d37dd2e4b84e0b99fbb0bd185de1511a8515404e04a8", size = 5802891, upload-time = "2026-07-28T10:24:59.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e7/283ae1bfd023d910e1e5a66388723ba41f2dcb60b55172467a21d0a557e1/uv-0.11.33-py3-none-linux_armv6l.whl", hash = "sha256:d629531a4e8f7cd76d861ec2e0035faef2675036a084d0a5b35dad5025ec62aa", size = 21679896, upload-time = "2026-07-28T10:24:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/30/99/559a61e7e891c3074b2cf2ae8481fc7bf2bae7de047e7c9683f3ba0e1b27/uv-0.11.33-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:94fb388a86cf6c2f2610b427f1bf0897bc0f2771a869da41433e17f7d21a1a41", size = 19989546, upload-time = "2026-07-28T10:24:15.049Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e5/17a4e36299e9bd5e8101680be697c7832afac686d1fe8b28be28046c1d95/uv-0.11.33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8017991a398a55d177c33ecdb29beb33e7b53969921183e4681e3e5b278d73c2", size = 18324889, upload-time = "2026-07-28T10:24:17.589Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/166e7ce2bdb754e5dcb991773f16b54dc1a2339ac78c2a36414cc69db93f/uv-0.11.33-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:36a98c68ba5c3bb59469414f54aaf00bd3134a647ca34509bc53d133ed8e0b5d", size = 21141300, upload-time = "2026-07-28T10:24:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8e/c95333b21900b3bab223205aaf99eb3415f1fec5203e68fbb55dacf7e818/uv-0.11.33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ab9ee61ed9c7c843b52795a96733e109cec831cd3250d73d291bccbb90a81999", size = 21202069, upload-time = "2026-07-28T10:24:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/cf86cd18bf815793ee33878fc9ca9fe5506eb2cb31580c46da272e2aab41/uv-0.11.33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:923874411bc0bbf5d1de16cdb14a7800d696d6c6e774eb1b202966243c6d507b", size = 21234505, upload-time = "2026-07-28T10:24:25.706Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7b3101d4bb8a70f4ac495e199fe26304357dd94610136083f5e92c408dff/uv-0.11.33-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e00f371e1affe3fac88f9f1afab31139c8199ecf8df7b1a37554f83629fd7eab", size = 22019144, upload-time = "2026-07-28T10:24:28.769Z" }, + { url = "https://files.pythonhosted.org/packages/97/09/1891a906d55dc146e014aa60badc87e74e06c9589ff96505e08a45e033cc/uv-0.11.33-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4196584a7f7fbfef3dde1b1a0454c174399bd96071cdfb066c263a798a4524e", size = 23159525, upload-time = "2026-07-28T10:24:31.332Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/18fb0f561a59f0e14fb7ce64dad3a6994b37473622e5deebb2434fa59648/uv-0.11.33-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70826563c7617efbf7244946354628253e4ea63d72cb1d35b98be4896c4530fd", size = 22768153, upload-time = "2026-07-28T10:24:34.466Z" }, + { url = "https://files.pythonhosted.org/packages/a3/cb/f0ccaba1bf9b0f778dc883e56f3bfb315e3d974ba36fb7432a067b459528/uv-0.11.33-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9542178978b0b6f16a7ae99e55aca039f493a1edb373a15d7993eab80a28615a", size = 22212804, upload-time = "2026-07-28T10:24:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/aa413404a2e9731ccf64dff368875d46395957e12eb129d2d2203f184ef1/uv-0.11.33-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:71d00a28beb3924c593ba7ec248263679ae1f7c0f63a8cccc75e7d11ccb2adce", size = 21276985, upload-time = "2026-07-28T10:24:39.468Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fa/71d5502c2fc0f6ce34d36a3e486914af6e05203e2e99c092fa469d2569d8/uv-0.11.33-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:ad74bd02a236417df9766b913c2613a7c3db0ef071c4c2390d28e5005be3e4b2", size = 22023545, upload-time = "2026-07-28T10:24:41.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/757180a4ed21f956a93e37de46fe3b4eb0a474e7eea5ddc904f4175d29c4/uv-0.11.33-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:686ed8c8d9e76eb9259e7f7db627ea836420d9a5e013b169c41398bcd28ee948", size = 22143736, upload-time = "2026-07-28T10:24:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/3f49cbd8631f2abf330d03f5fc687a9c35918c62cbe25ab5921b57dd2c76/uv-0.11.33-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c1ca9b0b51cd89310f87f1fc0bf9d9c6fdb64be516fe3e409817662ec3d9378f", size = 21164825, upload-time = "2026-07-28T10:24:46.992Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/7170288fa059340bb58ac5574f65d8c689afe3c2baa4099b5fa6cd304b73/uv-0.11.33-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2a8506f558e08498c0d80857c356452d14048eddfcc7bc20ec9f201b3011eaaf", size = 22412054, upload-time = "2026-07-28T10:24:49.466Z" }, + { url = "https://files.pythonhosted.org/packages/c8/91/86a61485e507d14a14bed223706db4fd6d95c99b452ce881a0122f2b018a/uv-0.11.33-py3-none-win32.whl", hash = "sha256:3242c8bc708b75fb72687c9a24f0e6f47c2004a3d82d32a81b06e17822708c28", size = 19394668, upload-time = "2026-07-28T10:24:51.848Z" }, + { url = "https://files.pythonhosted.org/packages/dd/be/b90df95297cdae2cbd5a83fddf429a304b17a0e9c277ca02190d995cbe09/uv-0.11.33-py3-none-win_amd64.whl", hash = "sha256:521229afa69ad5f57127de800120cb2bea1ac729a05a0851aaf920124f8edf66", size = 20185845, upload-time = "2026-07-28T10:24:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3a/061c8abd15dbb60a1dce2e28b60378a70fd18152718715cf59a3799f3045/uv-0.11.33-py3-none-win_arm64.whl", hash = "sha256:3cb5b325c58a9ee5febfedf4895e0c48cc7d22d801d1b23a4b8335f16508d0da", size = 19123673, upload-time = "2026-07-28T10:24:57.05Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9", size = 400471, upload-time = "2026-05-18T04:31:08.908Z" }, + { url = "https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4", size = 394599, upload-time = "2026-05-18T04:30:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/18/52/752dcc7dc817baef5e89518732925795ce52e36a683a9a3c9fb68b21504e/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631", size = 455458, upload-time = "2026-05-18T04:30:29.126Z" }, + { url = "https://files.pythonhosted.org/packages/12/48/366ebbb22fcc504c2f72b45f0b7e72f40a18795cc01752c16066d597b67a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994", size = 460513, upload-time = "2026-05-18T04:31:40.85Z" }, + { url = "https://files.pythonhosted.org/packages/ad/44/1f9e1b15e7a729062e0d0c3d0d7225ea4ab98b2267ef87287153be2495fc/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e", size = 493616, upload-time = "2026-05-18T04:30:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/7e/55/8b1086dcc8a1d6a697a62767bd7ea368e74c61c6fd171683cfe24a3fe5d2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19", size = 573154, upload-time = "2026-05-18T04:30:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/14/7a/242f400cc77fafa7b18d53d19d9cb64fc6a6f61f28c55913bae7c674d92a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8", size = 467046, upload-time = "2026-05-18T04:30:41.869Z" }, + { url = "https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07", size = 457100, upload-time = "2026-05-18T04:31:13.064Z" }, + { url = "https://files.pythonhosted.org/packages/81/36/519f6dbb7a95e4fe7c1513ed25b1520295ef9905a27f1f2226a73892bfb7/watchfiles-1.2.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551", size = 467038, upload-time = "2026-05-18T04:30:32.915Z" }, + { url = "https://files.pythonhosted.org/packages/2f/12/951af6b9f89097e02511122258402cb3578443021930b70cf968d6310dc0/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310", size = 632563, upload-time = "2026-05-18T04:30:11.539Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/0cba1f0a6117b7ec117271bdc3cb3a5a252005959755a2c09a745e0942cc/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df", size = 660851, upload-time = "2026-05-18T04:31:53.186Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/26347558cc8bf6877845e66b315f644d03c173906aa09e233a3f4fd23928/watchfiles-1.2.0-cp310-cp310-win32.whl", hash = "sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1", size = 277023, upload-time = "2026-05-18T04:30:18.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/68/a5e67b6b68e94f4c1511d61c46c55eba0737583620b6febf194c7b9cc23f/watchfiles-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d", size = 290107, upload-time = "2026-05-18T04:32:09.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[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 = "wrapt" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/31/5822ce37ca8820c2ed35a498c67c8b37960b9cee2ba437fd32849d0a234c/wrapt-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7", size = 81191, upload-time = "2026-07-28T06:04:04.858Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5a/3c6117938be98754578ab83f5a40d7d0ea2cd2c487dc5cd6027ee7228229/wrapt-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f", size = 82255, upload-time = "2026-07-28T06:04:07.151Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0f/94ae724c5087eb6054c0d63febd7094947dcf302fe058e2e0488102a872b/wrapt-2.3.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43", size = 155228, upload-time = "2026-07-28T06:04:08.272Z" }, + { url = "https://files.pythonhosted.org/packages/6c/21/1f780bba935dcf697c0c59de9be3a559bbb8e31a53ca3f25422023738432/wrapt-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023", size = 157073, upload-time = "2026-07-28T06:04:09.459Z" }, + { url = "https://files.pythonhosted.org/packages/73/31/6c7799d7b6431fcd7e1b83245fb45258a2d2c3a2187fbaecb83572a72d7a/wrapt-2.3.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152", size = 151594, upload-time = "2026-07-28T06:04:10.784Z" }, + { url = "https://files.pythonhosted.org/packages/ce/17/42d670dbfafd49076c6eb2b7d67633d7e1c968e39bfb11a135acb6fac67b/wrapt-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02", size = 156069, upload-time = "2026-07-28T06:04:12.316Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d6/c66b4ba4eda49257c84d5c2df26118280f09ca7905aee20d0064db778d13/wrapt-2.3.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276", size = 150930, upload-time = "2026-07-28T06:04:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f2/1a3b949c0322fb27396eafd1044328c1cb0400e0b32105d75a3cd03096e7/wrapt-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199", size = 154525, upload-time = "2026-07-28T06:04:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/147563a3dfa6e830c857b93b530ebd8c0cd9d540e5914aec8f9b12880c02/wrapt-2.3.0-cp310-cp310-win32.whl", hash = "sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35", size = 77879, upload-time = "2026-07-28T06:04:16.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/eb/921405b4dc55d4f8be4c700ef120539fdd75d5fdb50d83bd257171ee18e0/wrapt-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0", size = 80733, upload-time = "2026-07-28T06:04:17.43Z" }, + { url = "https://files.pythonhosted.org/packages/b6/13/75947450c5bb57795fa86384721cd52c5c4deb0879022f309501a8a85d44/wrapt-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760", size = 80199, upload-time = "2026-07-28T06:04:18.761Z" }, + { url = "https://files.pythonhosted.org/packages/00/b8/9182e4c618a847be0baccb68e4602b070d0fa22c782cf058f4bc66b32709/wrapt-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe", size = 81427, upload-time = "2026-07-28T06:04:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/613cefd9c5977366b1587e61c0b428176d382e6d75b454084c5e58503042/wrapt-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d", size = 82360, upload-time = "2026-07-28T06:04:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/71/71/4cd2151a236f44a6e2dd4ed8011838d7ba0be3d656c8bafdfc65a2ed1917/wrapt-2.3.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8", size = 161700, upload-time = "2026-07-28T06:04:22.723Z" }, + { url = "https://files.pythonhosted.org/packages/49/2c/bc508fee75eb2919ed69769800b09968e4aab16897f909a23f39c81e323f/wrapt-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c", size = 162922, upload-time = "2026-07-28T06:04:24.177Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e5/04f34d38e66d857dfc2fc4088d60e70c0e422467822defa49b2b4a26e17b/wrapt-2.3.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731", size = 156125, upload-time = "2026-07-28T06:04:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/23/41/c35940ea1c423f129ebe4361db853bc80d4def6326242e1206fa15bf94f4/wrapt-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb", size = 162039, upload-time = "2026-07-28T06:04:27.154Z" }, + { url = "https://files.pythonhosted.org/packages/0e/60/9bda34c3d7d182aa703fe35339ae0ed4c4dad5e5c587f93890143e1f87fb/wrapt-2.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6", size = 155110, upload-time = "2026-07-28T06:04:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ba/60bfd9b1a751f4fcb2d603668fc272d651ccdd339a56acf8c40ad21a0293/wrapt-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd", size = 161089, upload-time = "2026-07-28T06:04:29.959Z" }, + { url = "https://files.pythonhosted.org/packages/0f/32/2bd358c6f4f1305c813479d1e9ba746bebdd794f4a20107ab2b3ee0cbd45/wrapt-2.3.0-cp311-cp311-win32.whl", hash = "sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14", size = 78030, upload-time = "2026-07-28T06:04:31.241Z" }, + { url = "https://files.pythonhosted.org/packages/4a/62/ecc969b13b141fef89b888c9760821cb01a86ac8fc953911592c8e1e1522/wrapt-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84", size = 80944, upload-time = "2026-07-28T06:04:32.655Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3d/9278ada8a2b3f24372b630361e84e9a7de7abc3784634860c26d1c37785a/wrapt-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98", size = 80074, upload-time = "2026-07-28T06:04:33.811Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4a/d17a0fad1bf1c5f2c887ff71fef75654141b0880bff71d157d955b5bec3a/wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525", size = 82139, upload-time = "2026-07-28T06:04:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/51b92daaf6defb57f4dc56bdcce985400f75c6984a03ca5e78ccac717028/wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d", size = 82723, upload-time = "2026-07-28T06:04:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/28/7f/cfd9bc4b1f5e424eeea83d0493e43f3b1b02707ce8e50c47945873982bd5/wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8", size = 172381, upload-time = "2026-07-28T06:04:37.674Z" }, + { url = "https://files.pythonhosted.org/packages/cb/89/ff7814f6eb6856b479946117d1138a2fbb46cdb6b1f379db359056c69743/wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb", size = 174120, upload-time = "2026-07-28T06:04:38.987Z" }, + { url = "https://files.pythonhosted.org/packages/12/1e/8eded8615d39e3ce81f626937a3a87b280a2a86239a2bf14a4b4bb345034/wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60", size = 163035, upload-time = "2026-07-28T06:04:40.361Z" }, + { url = "https://files.pythonhosted.org/packages/35/ea/a0af2d9da62897af2a055484920de05dade30d2ba2c0d65cbdea875d3d8b/wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02", size = 171887, upload-time = "2026-07-28T06:04:41.614Z" }, + { url = "https://files.pythonhosted.org/packages/7e/dd/63cd4c864c65ef4906df64bd2d378f4a62b54f28063f282dfb3bf93caead/wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3", size = 161113, upload-time = "2026-07-28T06:04:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/82f1fc9e431b5c2c5a6d201aa865dbeae3984c311c6d11a185f0c8367cf6/wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d", size = 170530, upload-time = "2026-07-28T06:04:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/37/a5/5dc590e863a419930d988f8b7ca3e75a6befcfb10b6003b3a152f3d5f732/wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1", size = 78323, upload-time = "2026-07-28T06:04:45.484Z" }, + { url = "https://files.pythonhosted.org/packages/51/f9/4a6925a07951df56394f7e6ebe14f69f1c5ef9d87aa63e0839acf15aa63a/wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8", size = 81180, upload-time = "2026-07-28T06:04:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4f/8b5de0395b2a72216751d41c9861df6facaeb611b619d8810ed2b3b23eb2/wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab", size = 80155, upload-time = "2026-07-28T06:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6e/0f88a072483e76b881e3fdcd6b6ffb4a5791002514fe541e72b1b73c859a/wrapt-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f", size = 81960, upload-time = "2026-07-28T06:04:49.622Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ff/b7e2776e7c294075eb712cc9ef573d1b818f393006d09787262b8fc871c4/wrapt-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f", size = 82435, upload-time = "2026-07-28T06:04:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/343bb5d0f1f9669bc252a6073f085b4abf862511bd5c9c9eaec754341f1d/wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5", size = 170350, upload-time = "2026-07-28T06:04:52.187Z" }, + { url = "https://files.pythonhosted.org/packages/59/f8/13b79a392930bd0dd6b86cbfbfe1c40944110456e1dc6d809e5c46ece904/wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0", size = 170022, upload-time = "2026-07-28T06:04:53.599Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/4f1b6918f5290db959d6e0c07f77385d87cede29c39c9cf8f145e9c82954/wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609", size = 161043, upload-time = "2026-07-28T06:04:54.936Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/45d3cf74414780bdff6d0380467e003f6eb0f028b6c9403db868dbc7209c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8", size = 168576, upload-time = "2026-07-28T06:04:56.261Z" }, + { url = "https://files.pythonhosted.org/packages/f3/73/2fa58dd97f191c997755e2c6d569a68f0c433db4e4b36099bdd7227b6cac/wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae", size = 159140, upload-time = "2026-07-28T06:04:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/29/a8/08a56e2000a8816d449dcbad8c8b081697acbbd490821ceca0f9d8e8d20c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3", size = 169263, upload-time = "2026-07-28T06:04:59.161Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d4/354e1725e35a73b2af4fa70a3e024c7a5d1bf1802dfb862dcb668aae0253/wrapt-2.3.0-cp313-cp313-win32.whl", hash = "sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f", size = 78241, upload-time = "2026-07-28T06:05:00.507Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7e/34c87fa2174848dfee820322aaa318bab08913998ccecc8d2f57b4ad4639/wrapt-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838", size = 81113, upload-time = "2026-07-28T06:05:01.839Z" }, + { url = "https://files.pythonhosted.org/packages/11/86/fcc9a530579e008c9478bb565a6cdfbfd33536660f069c8b91a6607c5050/wrapt-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579", size = 80182, upload-time = "2026-07-28T06:05:03.152Z" }, + { url = "https://files.pythonhosted.org/packages/96/50/3864848b95b28ef73e17551fc8dccbff2628a834f52cf26a57f9c419fb83/wrapt-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944", size = 83921, upload-time = "2026-07-28T06:05:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4c/3d1921a60c3e8c71c540ff136e6a47a1fbccf7f671e818394889f7871d9c/wrapt-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360", size = 84412, upload-time = "2026-07-28T06:05:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1a/4a796ff7adb26ada6d4b758c94d47a38320b085e7099afc088efbbcdb006/wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614", size = 207168, upload-time = "2026-07-28T06:05:07.256Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3e/d7777776806c579b761bac2f91721dda9f04c7a1b380213c5935cc750ae6/wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a", size = 214351, upload-time = "2026-07-28T06:05:08.945Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/2d64d394df7bf181955b3bb562bf33c4492fb4be113f53071106d43ad8b5/wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687", size = 199020, upload-time = "2026-07-28T06:05:10.418Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3d/fb31d3db7d9834d265fb1a27a2adf0ddf51557c67458c97b22439ad6ae3d/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570", size = 209969, upload-time = "2026-07-28T06:05:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/8724b5da582e62070dc9bf4d8bf1972f317297eefd7ba1f2b5c6393ccf6c/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41", size = 196324, upload-time = "2026-07-28T06:05:13.557Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/3d9ef411149543016ee6bcf3af707f787cebd946527452b94bf122e9b7b4/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4", size = 202610, upload-time = "2026-07-28T06:05:15.048Z" }, + { url = "https://files.pythonhosted.org/packages/13/9b/4fc042ceb757866dd4a5fc057b3b736f2b360d3703ce9f830d83dc9226e0/wrapt-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3", size = 79178, upload-time = "2026-07-28T06:05:16.469Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/b94878f8eed809ca042685276bcea9f24e8c2ca7c9653bb80bbb920a68a5/wrapt-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98", size = 82634, upload-time = "2026-07-28T06:05:18.026Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/663e1de5332a71685a729754312d327d4cada767c36e1c5a2db4c8de49e6/wrapt-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6", size = 81387, upload-time = "2026-07-28T06:05:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/58/10/b073beaea89bc0d3670a75ff51139430a54b6af7ba7796507730634536dd/wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc", size = 81978, upload-time = "2026-07-28T06:05:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/0916d9cebf848ed3f1a0c1888faee421747df77331e4db2bc527a9a85988/wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1", size = 82518, upload-time = "2026-07-28T06:05:22.562Z" }, + { url = "https://files.pythonhosted.org/packages/f5/73/31c1bf0f3384062751c2094dadb314916d70aa9b6bfd26d994b4a7b393fa/wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945", size = 170187, upload-time = "2026-07-28T06:05:23.904Z" }, + { url = "https://files.pythonhosted.org/packages/ed/25/fce087d54b79b8905f3c3c9dd5f454bbd8d8acb80b960c4a6aee5b4659b3/wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5", size = 169288, upload-time = "2026-07-28T06:05:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/0d09e6dddc6b7a7230ac77f50254b5980ab4fcd22976f72f8cc8a0404458/wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3", size = 160932, upload-time = "2026-07-28T06:05:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/0913af0d2ec0c43865d32d615f518fea66c13c5c930e489e9b0de248e9a8/wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07", size = 169017, upload-time = "2026-07-28T06:05:28.501Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f2/3d1e47ea81b822210f5df1bf942fd90780a75c055243d569b664529dea88/wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f", size = 159065, upload-time = "2026-07-28T06:05:30.01Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/ef2066ced8e5fca204e2b361e9708e36555b40949c583d997ea3b590817d/wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23", size = 168821, upload-time = "2026-07-28T06:05:31.649Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/016104650d4e572fa91506eb396b3dd8efbccc9284fdc1c9479c3d21db28/wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b", size = 78700, upload-time = "2026-07-28T06:05:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/6fdc20a9f2ca304748b3f0819cbf377d55260562777bf0b615431bc3c181/wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d", size = 81422, upload-time = "2026-07-28T06:05:34.774Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a4/9cbd53bf05746bea2c392af39cb052427a8ec95cbd494d930733d8f44681/wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab", size = 80639, upload-time = "2026-07-28T06:05:36.228Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/6c5e4a0f66ea0d2b2dd267e8dd05a0014eea56840b3c8595d40b0a5d1f91/wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84", size = 84030, upload-time = "2026-07-28T06:05:37.714Z" }, + { url = "https://files.pythonhosted.org/packages/6a/eb/a1aedf03283bc9cbf8a1783995ddc54e3c5a86878f19002d2c428494f4c5/wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7", size = 84419, upload-time = "2026-07-28T06:05:39.131Z" }, + { url = "https://files.pythonhosted.org/packages/63/61/50d511c0dc5105563849e86daa3e16ac7feef699f79fb05af45ea70107d5/wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2", size = 207171, upload-time = "2026-07-28T06:05:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/3f/59/9b538cf7795217e810699d16bc88b96a830d9b5c403eb2ec2db6b5f2ae81/wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c", size = 214329, upload-time = "2026-07-28T06:05:42.287Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/9935d62b1499e5c8b3d191e99ba4eb31ca237a0b699142011a837e9dc7ea/wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295", size = 199079, upload-time = "2026-07-28T06:05:43.958Z" }, + { url = "https://files.pythonhosted.org/packages/2b/01/4446b80fa2ffa47a3449b250d004ba1c1937f07f64a179608fec735df866/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd", size = 209992, upload-time = "2026-07-28T06:05:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/d4/07/56f26c9f9979586a021e8148747004aba4498f49458c90b0502969b904e1/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df", size = 196334, upload-time = "2026-07-28T06:05:47.608Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/6d7bcc895b0f28b2250e10908f060687b9165429dcd7f22ddb3d4c031b74/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109", size = 202644, upload-time = "2026-07-28T06:05:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/cd/25/7860927edba06b758b8852a6f02e832be715563c67a6795d94350bc81099/wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501", size = 79685, upload-time = "2026-07-28T06:05:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0f/270bafe92fde3b069a39bc01e39ee79340895b335640df861d43d2a51885/wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5", size = 83104, upload-time = "2026-07-28T06:05:52.405Z" }, + { url = "https://files.pythonhosted.org/packages/55/b3/af176d79a8515a8a720eccdad9a96f6e31a30abf2865430c8c42adf2fd13/wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51", size = 81774, upload-time = "2026-07-28T06:05:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, +] + +[[package]] +name = "xxhash" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/a5/1386f35da1475fcaeef42581deae73417c6d2a6a0b2d2e8914de18844dcd/xxhash-4.0.1.tar.gz", hash = "sha256:d55bf4ef10eb09b8b6866790e083d26d087d84caa3cc0946ba87c3ca7ecaf7b7", size = 101513, upload-time = "2026-08-17T08:24:08.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/de/6854b311cb468985de555e49717357ba1de8ffe4b08eda3f4c522a418fda/xxhash-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3f68fe400ceec235f3e4a4b02a28c2fd2d283584a193223c921dd4c48f1d0754", size = 38470, upload-time = "2026-08-17T08:20:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/91/2a/7d79952633fccbf9c2a59cc1dfe9d073f9edfceb052ba9bff5075e31408d/xxhash-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b1dddc257279417d93c9e59420d49ef90aece90d7a01996db3aade74b0281b1", size = 36229, upload-time = "2026-08-17T08:20:10.259Z" }, + { url = "https://files.pythonhosted.org/packages/70/6a/6a6f4220c39bae1636eb3f8ef217359b8afe268493ee858adf9002130257/xxhash-4.0.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4972332c079d6aad69c4620a68d015a4ecb33141583f70d642cf9edf6a713763", size = 243168, upload-time = "2026-08-17T08:20:07.211Z" }, + { url = "https://files.pythonhosted.org/packages/c8/03/a1d745b324bede7af8a274d889d25b758402629695499a957375cc5fe994/xxhash-4.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1b603d0686c99fa0879f104a74e7db58367634c6e50ba827bee9aa095e23205", size = 266061, upload-time = "2026-08-17T08:20:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/55c7565db23d2d29a2e31ea9dd0001ad3f4729298f42ba32967dde7644ab/xxhash-4.0.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:33fd538191f47071deef6b1f676535e2aa770f1fd150ae4cc75a34c9e930be3d", size = 287357, upload-time = "2026-08-17T08:20:08.901Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/c967c98cbf151df6ae8756fc9821b1f78d1feffa8420ff2ad5645f27f98f/xxhash-4.0.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33e270d302c95ec426dfa0f5a4e16bff2ab8d7b8a46faa4746affb05e684ac77", size = 270683, upload-time = "2026-08-17T08:20:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/70/98/df9f3c14bc2fb2712ed1497349dc7e8c0b0f20a1f353e2e146d83a1adef9/xxhash-4.0.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3fb1d30d4b6d6e2c4a08e5ac6fffdb2b572d2cfcca15a5509cf4e7a1350f955c", size = 497802, upload-time = "2026-08-17T08:20:17.217Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3b/e5d20b4f602d15fc5fc066f601052a7d6f5a401a50d67fac92098a5829a5/xxhash-4.0.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e27dbed5c4ba033919e4b4ed8dc14e029e91d14a93cd9f920d25277c7df6781", size = 249503, upload-time = "2026-08-17T08:20:40.14Z" }, + { url = "https://files.pythonhosted.org/packages/39/98/4cd03fa7ba791657aa555d0655e89a0d66193b1331e4391114b50227f80f/xxhash-4.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43bcf2a871f28f16135545415cab3ec43904d4c80425a64598a9e6cebfb2b5ba", size = 333384, upload-time = "2026-08-17T08:20:18.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/67/a0a076dbcecad2af0cceb873ba4f95475f3955fb81d82f9c5e9f97a12854/xxhash-4.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dc434c946012e6d8a72b10f970ea30755b718251dd7591dbfdabafd3bcb21bc", size = 262463, upload-time = "2026-08-17T08:20:08.719Z" }, + { url = "https://files.pythonhosted.org/packages/48/94/767b68736250813923d74703ef628b0f57ff1c5bf690d7a6082928d1a14d/xxhash-4.0.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0b1082fd0f089ce9098ed77aad8b777b5d156f8ac601c69cab73811822b8ef07", size = 289405, upload-time = "2026-08-17T08:20:35.493Z" }, + { url = "https://files.pythonhosted.org/packages/ab/61/69d9ec6e85f9933f26b39753b404f97ffd2fc3bfeeb5e26333752595e060/xxhash-4.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd649663ddeafbfd4734eb8abae921dd5baa1242f20bda54e8bc927369ccded4", size = 247989, upload-time = "2026-08-17T08:20:10.759Z" }, + { url = "https://files.pythonhosted.org/packages/a7/66/e6a033879893074bc441faec65587f9ef9cca49ee3cb99ef20c699b014cf/xxhash-4.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:97b455de3e8b1b0b1e4594cb61a468992563f03ca264062fbb0a66b393c01d90", size = 267987, upload-time = "2026-08-17T08:20:20.947Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/a99a412b010cf2cc2bd38a0a44043c894ffa80b46c8d4e1522e40d2ef401/xxhash-4.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2194bf96d5f3d4e0cb65deba370ec83dda3edfba42155f9384190ed5e51ea5e2", size = 322541, upload-time = "2026-08-17T08:20:22.675Z" }, + { url = "https://files.pythonhosted.org/packages/0b/12/1a91408e66d1716bd278cf55529d4ba251d90c6b694ea78a09dcbd9d36c6/xxhash-4.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:84df5f8da574caadbc0cb1b8866ecc2368cc941f0cd05f677756c802f370dafa", size = 466264, upload-time = "2026-08-17T08:21:12.422Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c8/8abd4c1b90962f794ed9b139767b0a971e5168e8393961ccf046956c0039/xxhash-4.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c09ada495567c9c9a8156c5ebcfb93be7fece0755062d738c972dcbecd0d84b5", size = 246149, upload-time = "2026-08-17T08:20:23.385Z" }, + { url = "https://files.pythonhosted.org/packages/22/3f/e020285d737a712bbfcf01dfdbbec7773c7c8289447a5373540caf2cdf5f/xxhash-4.0.1-cp310-cp310-win32.whl", hash = "sha256:85bdd40cb505a11e0ca04191711266c5fd696ed786ae83849955e457774edc96", size = 34630, upload-time = "2026-08-17T08:20:10.001Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9b/4e8384b299b40664a225ce9deef54c42b3d59f5181a16d4a374ea3503f8f/xxhash-4.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:8ec4777d92fd61a5c8fdeddab894fd65bea301a8092fb5419ec6472aa4d458d7", size = 37058, upload-time = "2026-08-17T08:21:05.846Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e9/d73c870d9dd26f8d6782ecf716b31e836bb6269c888b6bb6e1fcf71eed36/xxhash-4.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:03600a8987849b2bef7be795a60a6052b635c63fa98b718b08ca5ee823691cfc", size = 33289, upload-time = "2026-08-17T08:20:19.707Z" }, + { url = "https://files.pythonhosted.org/packages/0e/58/bc81e25cceab76ce4b400441e3a43312bf3887fedbb2e5f80cc5a7dd7f75/xxhash-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b4477edc03091f51f5309406d230851c23cf4822029e3bf40b8df53093fff1c", size = 38473, upload-time = "2026-08-17T08:20:34.303Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/d95e810c9a2930906f1fbd0e38f77554abb8e042a190aa9cbb24da39e9a2/xxhash-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:04f9a24de11a6647666d5302fd73d6a5224ce50ddc965fb0bb44cee736e6bd7c", size = 36228, upload-time = "2026-08-17T08:20:36.641Z" }, + { url = "https://files.pythonhosted.org/packages/72/be/ebcded2a32ba664a17a1737a91b6baa298bcda6942acdad81af81660b74c/xxhash-4.0.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8c5ce76b94ba49f3be8a8f2611abc6564210702c72ac9e237ca2bebfd17794", size = 253292, upload-time = "2026-08-17T08:21:27.206Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/72d31d787d988d1130253bc34d249c553f02c9387e7dda789b6d5aa7963b/xxhash-4.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4c8842fb19d78b5e8c2a52baf4c8357658cc56c62bc822b86ce0f942f28e286", size = 276545, upload-time = "2026-08-17T08:20:26.726Z" }, + { url = "https://files.pythonhosted.org/packages/39/ae/048f3b1f283a340bef3697fe5a9d4f10de1696a379af9af6b2352c24d82a/xxhash-4.0.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a43418e1a90b4809a9caf64aeb8b0696e3e1f300a323acc1e6ee2f93ae319fcf", size = 296295, upload-time = "2026-08-17T08:20:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/e9a593c81445c8e8d669402edb8264144624e7e5e2406df7d33e3c164d90/xxhash-4.0.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3662719007e059abde7eddacf8517142ba076ddc7b30c807260e57d28c3c191", size = 279966, upload-time = "2026-08-17T08:21:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/29da2b166955a300521c0abd498ad4481916c3743949b106192b781f58fc/xxhash-4.0.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06d7fbd609503c3be5e65cdb6bb2f040d6a98574404e2e1d5c60815c97fff4aa", size = 509850, upload-time = "2026-08-17T08:20:32.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/ff/e39e1900179ce7f0f23e7000d9fc672471a8d05b6b2dc657cb496c8975d0/xxhash-4.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:101aa300de6ceef3d9c77569706330d8921fc45dd82bceed2084f1e9f2557a24", size = 261005, upload-time = "2026-08-17T08:20:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/89/79/76ee26720d13219458f4b2ec0b22f539fe2bcb1f83dc22a24be4cda4e285/xxhash-4.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4296fcc790876a8b0f297edc83d3b088457b774d8f67b4636807f8a2ec69a79", size = 339620, upload-time = "2026-08-17T08:20:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c7/56b53252d64ecb2f3a0d283b41033561b1bec9c268095150153b694c2e52/xxhash-4.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:57d7fa8f23908d173001c21a9e82bfc6ad997d1b6c270fb121812b7ed158891c", size = 272558, upload-time = "2026-08-17T08:21:33.883Z" }, + { url = "https://files.pythonhosted.org/packages/fc/76/83101a2f2ad3eb6b4b5d571e0aa394a576e3e23121707d507d80197ac385/xxhash-4.0.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85e402dab0f9acd3604539747c6fcc57dc188a18af6ab07eb8189351cd32466c", size = 300417, upload-time = "2026-08-17T08:20:36.183Z" }, + { url = "https://files.pythonhosted.org/packages/34/5d/5be1166ec4fc4bc896e508dc51189a2dad200b373269a430e214fb152693/xxhash-4.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fb59a0dd61fb2ad481c03fda399d78ce57dab6bb62c2c8fdb446a7ba4754b89a", size = 259286, upload-time = "2026-08-17T08:20:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/77/6c/42f6201f13278787c58a6b3a1cd597b47e8665a7d4f68a3440d001c05a75/xxhash-4.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0b20a06454b34f1531fc677c54efe2ecdec691ef9224f7fa919bf2c1363f7ff1", size = 278230, upload-time = "2026-08-17T08:21:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/3cb7f149a5a469c628604c33bec6d79764698b315700a4bbf1c6fb15622b/xxhash-4.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f7db035447a0ac8959aa230c5d36545ecf9f547413eb1711c0ca6f0ba1418925", size = 329846, upload-time = "2026-08-17T08:35:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ee/934eb1e11f4d0e95f3828825f8da4b6d59e338235d63edc3d929769262d2/xxhash-4.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:34ed93e20bfd98d722b902121643791eeb4b1641871e2dc63d0d4c2d93f187df", size = 477285, upload-time = "2026-08-17T08:20:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0b/77835dcd7aec970db74f5724c94207ada83b3e2f5e1474ab44b9c8fe5ea5/xxhash-4.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6247f5e23ee94f2557ac9dab738a336f607c6ff476fcf66ca70c3aef5eee15a", size = 257552, upload-time = "2026-08-17T08:20:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a3ce7a1a9c4ec9ad8babba591a996a71c474ff9630c5fb04c8c9d8b995ca/xxhash-4.0.1-cp311-cp311-win32.whl", hash = "sha256:348c8f288dc961d6bbd1985c8152a3ed7a85c95df00e82320f0c5215d922a399", size = 34630, upload-time = "2026-08-17T08:21:53.323Z" }, + { url = "https://files.pythonhosted.org/packages/d9/58/60d2170e8cda0891aab25dcaa74797b420c3c6cde8a5bc8372f17b30c0cf/xxhash-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ac0f291ab6485bd71f33941f9b92771318332a05d505460b41e893a549caadc0", size = 36992, upload-time = "2026-08-17T08:20:41.973Z" }, + { url = "https://files.pythonhosted.org/packages/ac/de/b229a39f9bbbe30cbcf9afaaa8993cb65286715c90a3b058c3769196ae02/xxhash-4.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:72f34834518157a75e7090f328ee7a16c70c804cfc7c694fa069cc888e9fc03e", size = 33289, upload-time = "2026-08-17T08:20:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/26/6c/dc7cffeadd06336cd934947187cd38abb263103bbc552ca0f55fe4ff595a/xxhash-4.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1ee523f51718e41753f04f7102bb4dc55a18d2ea5cbaceef8ec7ca08571bd428", size = 38444, upload-time = "2026-08-17T08:21:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/75/c9/cf736f6db8c3273af18925061572db0d4357818a9ce425f4b5fb0021918e/xxhash-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:515a822c73abbf6a0b7c70976d9662be342835c9d78b8dc7c023411f39c35dbc", size = 36195, upload-time = "2026-08-17T08:35:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/da/a2/ca1929354b6851529d0148f7f335b5e2b0281f83bab3e19f0896dc579796/xxhash-4.0.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f5d031f35962e5483a613214e61f09fe24ab523062c3646d592dc16c4a217451", size = 253113, upload-time = "2026-08-17T08:20:52.152Z" }, + { url = "https://files.pythonhosted.org/packages/de/bb/542005206af59518bc8d78a210f1e0172217bc53beb32f64a5b632e72b6b/xxhash-4.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0264844a09b538c894e5eff25313d941deb4dedec2131b98418a71a3c9944e", size = 276525, upload-time = "2026-08-17T08:21:01.886Z" }, + { url = "https://files.pythonhosted.org/packages/1b/df/607cff25dcb0f1d35c3b04493f6ad8471edb03fd4eacbdcc5ceddef1f3e9/xxhash-4.0.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1642907941ee4b75aacc3db688af52ea02ca2305ab22af7ee686ed726b332684", size = 297703, upload-time = "2026-08-17T08:21:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/15/ba/9d2275eea0b9d9c6b02921be23f7588356c60df95c763b25f0e045894d43/xxhash-4.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4af350bc3f329970c0e3a59af84a8a30998bf8a9167eb50cd48e59baaa1d7bec", size = 280252, upload-time = "2026-08-17T08:20:47.299Z" }, + { url = "https://files.pythonhosted.org/packages/1d/aa/2299d9f6369e550aef2abb64945e39daa34412725aa46a20d99b74d76f67/xxhash-4.0.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ba782ca3bf1e81492611152b9a0d5264971339e95e34d69de0ac2c926be496d", size = 511041, upload-time = "2026-08-17T08:20:36.771Z" }, + { url = "https://files.pythonhosted.org/packages/83/97/31bd8b8279e6935a0719f6910ced15e9d5a2cd554b253f6027ce1b5a1c2c/xxhash-4.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:237b8f63a2a0fcfb1ffc06e21dad23add44e6d354b2b014364a1d41e419a4dee", size = 261812, upload-time = "2026-08-17T08:22:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/d180a2da23c105d8e0b02d54f9f5841013fc81c233010ec781e31f1aee4c/xxhash-4.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:81507a68ba84c55241fb61cce1469f473a5da4205fc8ef6f698e5948eea8dd88", size = 339878, upload-time = "2026-08-17T08:35:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3d/f584cd3172fe934f0f5a0a3917d0d7ce781f74d794fd43bb72be71c3ef6f/xxhash-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1ea31d61bcd2cd2f3ec4ca80a64187bbd7948f490b63cf0dcbc6e717b4c1e9", size = 272871, upload-time = "2026-08-17T08:20:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/2c7956b2b551682e00b9aebce9ceb0a991a131d65f9850c09f5f9760be2e/xxhash-4.0.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:06713a5aaf1d0905c5579416c020c02e42b3ceb931e86c7d3b7fb85403dee3f3", size = 301440, upload-time = "2026-08-17T08:21:35.911Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/0739f6482184a8026f4b022718f5f815d352059312e80696825433f0a8e7/xxhash-4.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8cda075b10bb3917b002c74a04f9e02b7d13b5bf732571404d51c52b11c7329", size = 260157, upload-time = "2026-08-17T08:22:01.416Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/b31a7bcf1d7d116842812e54f9b944843b4236ea4fa85634e8259f342212/xxhash-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c10b9206753b64aa791b35b201485477525b26fdec5bf86e8364c388a03e2592", size = 278233, upload-time = "2026-08-17T08:21:15.674Z" }, + { url = "https://files.pythonhosted.org/packages/db/e8/5293bae090fc6119dbc5fcf5c4cc0e1536394b52d73b7904d033836c73db/xxhash-4.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f3e1a44af01b6692de0ec6caba5f0bf93ceb36896e02b7fc00952c6ea7ef39e1", size = 330270, upload-time = "2026-08-17T08:20:51.128Z" }, + { url = "https://files.pythonhosted.org/packages/72/9e/e2ab12d40921f3f34c9317637d65e011aeababf8288356ea8d527de2c1d0/xxhash-4.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6fc415b5568bd9accc7187f1729a99707330c0a67a8b9f93c1149ed573ed75d", size = 478555, upload-time = "2026-08-17T08:22:04.183Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/c6148d39a49efa95f39b4cf0d41ef35a487f3b30f6fb1fc8fe8d8eab577e/xxhash-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96d8de55029d42251945531f6aa7590c32b48163c66a43bf29d8657d7446a377", size = 258174, upload-time = "2026-08-17T08:35:21.18Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fb/0b04b68d6c5bc71c7a2c344f1287327b67e607f28fbcfd937697caca64b6/xxhash-4.0.1-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:0163b5d259de23ae9e07b7eabf435ce4704f6f205589a2b154e6af4be985ce1b", size = 20767, upload-time = "2026-08-17T08:21:00.806Z" }, + { url = "https://files.pythonhosted.org/packages/a6/be/476092aba34d1fcd313e1613a3bb3bc692f253d167b54bc90049043b5034/xxhash-4.0.1-cp312-cp312-win32.whl", hash = "sha256:1216f7ba5683f17a89eb7dcb4bc50a0b743dfe1902278d7b3d0786f538118433", size = 34669, upload-time = "2026-08-17T08:21:49.486Z" }, + { url = "https://files.pythonhosted.org/packages/aa/02/f9413d94fae43cec6d1a74c4f12156c6f4a7f5fd50e1d34defebdee3dec9/xxhash-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c2d525a3afabcd8e3549d85fc7e111fde6bc302d06a1893fe73adb79823415e", size = 37073, upload-time = "2026-08-17T08:22:04.886Z" }, + { url = "https://files.pythonhosted.org/packages/c1/83/6fe93c1b95acf962bc61a246df09dc2dcce895ccfc1080c9f48d0b652b92/xxhash-4.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:86b2b12bec60c678ed8f5cca0258ad93a8928ebddb6ca7732f0875afe1451d1a", size = 33299, upload-time = "2026-08-17T08:35:12.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/c707286b527722f776e1fb81dd202c45623355ba1a2972337a2a26075b2b/xxhash-4.0.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:8c9fe122444e129881afd1d4d1c7ac0d3ce2d91b68c2b40173b6025ff1c31f9a", size = 43639, upload-time = "2026-08-17T08:20:54.945Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3b/bb71639a0f95635f61936a6f2653599c4261b645ddddd8d00f9dfe3613e2/xxhash-4.0.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:1f3346c5c287ac3c7f38b20380f55e8768230e7252af59fabcf3b87ab21e4256", size = 40657, upload-time = "2026-08-17T08:22:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/76f3f5385faa9886a36f21fcc603f40b4c0c40ce622382f133160c48b4d9/xxhash-4.0.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4e5141543c7f7fe3087500bbb4ac2845cb528a980aa91f8f1e661e2292ff4a5d", size = 34708, upload-time = "2026-08-17T08:35:24.614Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4a/f48f0e3e1b1ab072979fff2a5be899234e28090883e8b519d0b10215d708/xxhash-4.0.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f09ee747e2a5f876cc5ad56947734811828335e13b403dd8ea1e06d77a9dd48d", size = 35650, upload-time = "2026-08-17T08:21:09.337Z" }, + { url = "https://files.pythonhosted.org/packages/c4/53/b73d7472b196101ad1f57ed0674af3af803ac3e9ec2feadd650a7b262562/xxhash-4.0.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:acf52474b2494ef66dc7e0fb6d5e2b50c18313039ad4d275fbf9f9907c804bc5", size = 37958, upload-time = "2026-08-17T08:22:10.616Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/024946ad8fa532074af4e4380179da54b7ec9facc8bd0b279ec0fac4e63a/xxhash-4.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1b3cccf75eeb5b01639b2feadb042a8e07889293b7ca72fa2985e7dcb64763cf", size = 38032, upload-time = "2026-08-17T08:22:09.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/934af8d99bb5885711006bec30a691f728edd513d2c40f053f887d8e7577/xxhash-4.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd878d32f5c6cbce9783f8d6897561fb772211edba9dde49d85672b88ed45276", size = 35895, upload-time = "2026-08-17T08:35:16.53Z" }, + { url = "https://files.pythonhosted.org/packages/20/5f/a8011f6a1558f7ca66d9077bb4f192b1871afcea62fbd5733605d2015755/xxhash-4.0.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:41e579025a6e13a99e6d71e39c9cfc621a0dcdbbf19106325e145fa858f2d794", size = 259464, upload-time = "2026-08-17T08:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/9665a44397547e7a3d58c0942425a976d58dcfd4b538f33220a312bf6912/xxhash-4.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74379a577a9f3b6afbdedf1b90e5c7764467051977f18a326d7d607336d743bd", size = 283949, upload-time = "2026-08-17T08:22:17.003Z" }, + { url = "https://files.pythonhosted.org/packages/34/2d/78774141266457468f29f3f5803092df4db87d8148ba74e4debd041649db/xxhash-4.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:acb31ecdd1a97fab5cd39a84ee9f515e727d319f796fec48703b8339b9998360", size = 303898, upload-time = "2026-08-17T08:35:27.951Z" }, + { url = "https://files.pythonhosted.org/packages/59/48/d78d22de576b42528bff87c14207de50de4f0b888221a50ff7c9d675d670/xxhash-4.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b7875ac1a2edcb691f27642b8b94b904baa6bcecb7d79c72df2228ba8cb5c51", size = 287241, upload-time = "2026-08-17T08:21:13.042Z" }, + { url = "https://files.pythonhosted.org/packages/4c/de/7a1755a59c59fd46176f293bbdd99e399a6537ba9537fc723aa4d1bf6e27/xxhash-4.0.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4751f1d7eecae6b2d2a773630f1a7248f125c9a92a456694d03c15bceffc9d68", size = 519856, upload-time = "2026-08-17T08:22:15.35Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fb/76580c08e916507859b0f335393cb5fdc59452c4402edbc6bcca6e47e7df/xxhash-4.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a51b061d54cda8b83e62c44458bfbf0dabbef9b975dd9649952ba5076b9f349", size = 268572, upload-time = "2026-08-17T08:22:14.533Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/1abde3e07b8f2077a38b4fbfaf764115008bfe0ff03bc7756a52c9fd0607/xxhash-4.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:74a164e8b63f1e9cf35c9a7809d082b033d1a00e7375d5d814415436e7867e57", size = 344967, upload-time = "2026-08-17T08:35:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/80b6ddf0732eef48a8b5fe717398274794392bd6dbe82af38d189d214772/xxhash-4.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f5e5c6df4b703afcbe9352d238a51efd97c3b91fdc3a2052e40fdacb1e7505f", size = 279956, upload-time = "2026-08-17T08:21:24.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/e0/11cbc43c205bf81fad50d69c7319cd1b1ccc01a66cd4fb8766357126c43d/xxhash-4.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d54b8ae068af532c8cdf56abb9e09a60fbe7b10792444c9c27987bb6d3b450fa", size = 307583, upload-time = "2026-08-17T08:22:22.541Z" }, + { url = "https://files.pythonhosted.org/packages/1c/11/cf0bc07feb2791045b6ac075d4bf64f1a5beedef2f46ae70d7104d63a19f/xxhash-4.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1749f0688020209fe0d357ce1e1cd9ec9c6161ed0405ea949d24581c4c43fa91", size = 265848, upload-time = "2026-08-17T08:35:31.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c4/7ada4bea2a2795073dfc42d96842930efbe7a0c1857ef4b522e4e90e5d83/xxhash-4.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:94ac8a6b8c47951173f0b67bf862bcb971bf24e493b9fbbdb0e010cbbc7d9f54", size = 284409, upload-time = "2026-08-17T08:21:23.156Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f4/d8ce83dd6b99ccfbdadaf2db968ae40334d2e5f73a0297e593b9ddb3df39/xxhash-4.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a33de7633c948ab2dc144af370a66e7e7af29b425dcd0f7e4f59689fb9391b53", size = 335921, upload-time = "2026-08-17T08:22:21.802Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9f/f47d8724bd8bc45b395b06b7cacea2dae0d00031af1b707184a091161df6/xxhash-4.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:247ece770647c0aef080561fa996f9774b4dadce2d0c42eeb98229db7dcf820d", size = 487023, upload-time = "2026-08-17T08:22:19.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/2d87098f3371cc1e42dd04d2285ad56bca4c56667bc501bff02d2b9fd6b5/xxhash-4.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4553d36cc0b7fce1f35ba8a94dfd775aa3ed12f5eab2dc3b46ac75a0706b0bb", size = 264333, upload-time = "2026-08-17T08:35:27.001Z" }, + { url = "https://files.pythonhosted.org/packages/27/b8/93795ca5898ec7d7d0455283ad261c0fc76b4f0c0a69e86233bd7badb0bd/xxhash-4.0.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:87aa309a93bd5ec13f14309a305ff4e9bf74c5363fc46c264c0a22edfd5b0670", size = 20581, upload-time = "2026-08-17T08:21:39.207Z" }, + { url = "https://files.pythonhosted.org/packages/b6/96/926f7335a0a1647952c00421e8da877f658094f61336306c7cadc335c94d/xxhash-4.0.1-cp313-cp313-win32.whl", hash = "sha256:cba763d84b06bda2c38d5185dee76f1b9dfdc0789e96e476d9e10005526d0788", size = 34449, upload-time = "2026-08-17T08:22:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/8a5aeb811de093bab3434e77eff0e9461624a1a56a6a93d315d080aab2aa/xxhash-4.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:97b94fb29abf21f5f0bde15f7dbdd3a4aa2dc59f37026adc7b4bee8563b84375", size = 36520, upload-time = "2026-08-17T08:35:34.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/14/97f3c74000ca36955e9cb86f6d270dcd5848b5c65afa623453f5cf2d83d6/xxhash-4.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:08ed8da18cd4fd0a6a5d6a444852d8fbd0e565388a74a4937085451b5f1a312a", size = 33428, upload-time = "2026-08-17T08:21:31.713Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/ea406a02b561d3275232ccfdb3e29df80f7a65414940e3a15721c7bea40f/xxhash-4.0.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:af05a3f650220a6c59fa0ad2410249f2d2470a05225807c378fb67458693f8df", size = 43747, upload-time = "2026-08-17T08:22:31.37Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/b0c94d61ccf6b5d1f8847b58ef8f923125ac4919ed5bd0eb082750ca7cbd/xxhash-4.0.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:a6e3653df1a70b8ac4191216324242e4be2bca18c9a7c10934e1bd56dc7ca15e", size = 40749, upload-time = "2026-08-17T08:22:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c5/8085881a538983be0fd1c865d5df236242fea496044e2c8ca32b9f2ba39c/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4528cf80ebbbf57d40edfb31521ae265daa6dd636d615b1cf0ac86209579e59d", size = 34734, upload-time = "2026-08-17T08:35:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/d3/94/8803d13c968fc75ca434eea991d29ac5fd8a36b4afc9a6a9803c53933db4/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:90cb2a1c9cc503a054a19612b48ff6e8e47805f618bdb3224a07568aad03a37e", size = 35671, upload-time = "2026-08-17T08:21:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/ad91d7f0fd294190d37c08236fe661f5c4e3f83dcd1a121877a2e64681ce/xxhash-4.0.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a949b072ea59c6eca0811ccd9e95133cc50d2afda8d464b5b077c78f78efa269", size = 38094, upload-time = "2026-08-17T08:22:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/89/f4/2b7ebdc1869caca5f02c4cba8379b631050d3c3d4adb9187e4dc1a6b8d3c/xxhash-4.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:79a3203aadf39637869dfea1185227d8452844d78b837e54fb1117b4d34ba5c3", size = 38244, upload-time = "2026-08-17T08:35:38.081Z" }, + { url = "https://files.pythonhosted.org/packages/90/9d/f66cf6935f528e575f1ae4d6560d376e7587569747186f4fae8777cadc1b/xxhash-4.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9f3848ffaf010bdbabdbf4c25641fa258b6227ff27bc74a4d06edef521a4873", size = 35904, upload-time = "2026-08-17T08:21:37.358Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/34569d7b482f0dc060074faafd163c588f915cbc3e3e218f1ffd8a3ad340/xxhash-4.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9283d9dd6b44acad35118e2976fc763a065509e4118debdb61916ec322ed17b9", size = 259595, upload-time = "2026-08-17T08:22:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/a2370acfcd48732cf5c2b87f06cfbf7fa51c0ce0dd736bde42939eb9ebf7/xxhash-4.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c7c642a0f79c3e3cf2965475507574d3d1a50ec71060039d60cb87358667cb2", size = 284279, upload-time = "2026-08-17T08:22:36.396Z" }, + { url = "https://files.pythonhosted.org/packages/08/15/17d33c24e6c4a1c0b9ddc5584f0c25d51d48b34bacde1416a2235a19db4b/xxhash-4.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96dedccfb09a73a25751053a183159b88f4ee75f388df8166040c152ac0531c6", size = 303973, upload-time = "2026-08-17T08:35:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e0/4ec0d69ad5738729098a61e631b7ed2df22a922b0e03014b597c72bd863d/xxhash-4.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81664268dba92e037b740ecf37fa02f1cab4a391f93f28e35792b3341c60648f", size = 287535, upload-time = "2026-08-17T08:21:52.158Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4f9b17e7a9eb71c65548ecddd9c18b84e3c18ca41c4d436ad2a3000d3f7b/xxhash-4.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:839f58c5bd9989875be0fd28446dbf32cace2c2cd8bf2f6762acdc38a95cd1aa", size = 519257, upload-time = "2026-08-17T08:22:43.272Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/3276b3e743b8ddbed9c3f71c76d9dd6a75d72aa4e678b1447b635cfd92e0/xxhash-4.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa44b4c7c5d0ffa31356b4428659516c0e47647825c74079a296b3857b6d99d", size = 268190, upload-time = "2026-08-17T08:35:44.985Z" }, + { url = "https://files.pythonhosted.org/packages/08/d4/f1555de3c96721320930dbb7988c8482d82b85970076aba1a8d40e83ad43/xxhash-4.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e681a6fc7e4f715252b9b5acfb30536ec7dd1f75033a32dc617e6fa95af1a3fd", size = 345553, upload-time = "2026-08-17T08:21:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/ac/98/c28908f27007087b61139d290f908dd827ffd40b88af0c43f9e1a1a7ffd5/xxhash-4.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6301d92545c591ad31c3e050aa40a5f8a4c16413f1f9e6f9322c6f0f9d2b736", size = 280499, upload-time = "2026-08-17T08:22:52.236Z" }, + { url = "https://files.pythonhosted.org/packages/a9/76/3ef57622c65816348f8196273485baab4752aae064959901e85cd867e067/xxhash-4.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6efb8f21cc136c79b3e5bb747c8682d37916fb202cdbbc32182de5c4e47f821f", size = 307211, upload-time = "2026-08-17T08:22:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/5804504bbc808968e57d6a50286dd8f8cc06e0ddd6e4ab4b1dc89ae42f35/xxhash-4.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760de77279e9cf9c81d012ce0705cba13afccee9b09c480f17d778c8c5cefae8", size = 265865, upload-time = "2026-08-17T08:35:42.727Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ee/8572fdfd70e7aaaf150af899871c2cc0bb88c3295ca82172a31e04ca5168/xxhash-4.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a16a3fa6936e36bb1414d16a6bd012c9033e5161b68b426805b61d895392437d", size = 284545, upload-time = "2026-08-17T08:21:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f8/6eadcca0904660c848b466524e82a233d16c9d2d5258433aaf3546142d86/xxhash-4.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c3c4b9aa9a27196b921197f7daf9e6c1412739df06a99cfa6e923879362eff6", size = 336022, upload-time = "2026-08-17T08:22:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/27/df/4aa107b81602d6d6d09ab5a607c530d2d3a6b28e2e9a59b01875bd877c54/xxhash-4.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:863f3d3b44110f7243e86cf994aa5c5d88f2348b6e84ab4402fadadfbf9f7da7", size = 486671, upload-time = "2026-08-17T08:35:49.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/b7/b2bf9b5301e9cd5f2e335fea8da0f5cf209a6594cb1fe77754774ad4a6fd/xxhash-4.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63aa52659bc32bb9bd7cb5caf523b4d14429a477762cfac886132d687c1f80fc", size = 263744, upload-time = "2026-08-17T08:21:56.165Z" }, + { url = "https://files.pythonhosted.org/packages/0b/96/35b1c02177ae26234892c2310fb4822ba62411acccbf425ab8f9fd99354a/xxhash-4.0.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:67e57b834e07ed973cee7b6da1548ff28a56458d77696fd2a5f397f340694848", size = 20563, upload-time = "2026-08-17T08:35:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/51/c2/a06300b165fbd6b0cb4a9742987f2e997a9f447ce3bf7c6ac97b862ce62a/xxhash-4.0.1-cp314-cp314-win32.whl", hash = "sha256:b6c1f9c59bbe593f88a0aad30be4150f15bd57bd64efb95feeabcb8e563f1ecd", size = 35151, upload-time = "2026-08-17T08:22:44.283Z" }, + { url = "https://files.pythonhosted.org/packages/06/96/c5b37296b78f80fc97124c0fee0c7bbd1bdb6f3b18bcd8748bb113b2d8fc/xxhash-4.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:da544672efd9ad76077928a3e6c5d894e52ce82d3bf14002db4a1bf17d1a36a2", size = 37156, upload-time = "2026-08-17T08:35:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5e/248f9cd169c2fb62236bedfba246d213bce728f74901e99047e3f3c55875/xxhash-4.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:d0d24a4f3fb63852cd09af46ae4b7a4d00cc8b8615a046dca543786e728d1056", size = 34379, upload-time = "2026-08-17T08:21:59.446Z" }, + { url = "https://files.pythonhosted.org/packages/58/c8/db1d37c0da0324d0298f6abd931ca1d4736e049d9f2081230a8421da74d2/xxhash-4.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:349775ac30372b344d2338b2a168c0a1312a644194da25b8bec476d55761a128", size = 38656, upload-time = "2026-08-17T08:22:49.119Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e18998ec465fb977bc74272e5bf3c2e886c13b014cbef916cd607802c709/xxhash-4.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:43e5f9169e73d0f0db33b5f6b8554bcce69ac278c966daf83d5eb4eb2f13829f", size = 36306, upload-time = "2026-08-17T08:35:52.853Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1a/b83f86f8a987a3cbcb7e005a6824ff64aecae35abc1395a0d44ee16c3319/xxhash-4.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4a252fb862b0ae2590587e625f47a0e03da05cf0205e8830b67b6596c06038b1", size = 273729, upload-time = "2026-08-17T08:21:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/2db15aa8508e0cd5b632927a53b98234f24039ea65377e6cf996c06d2d4f/xxhash-4.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2df3ca8757dc381e75e90a4d7995a6324f58a923c7145220a7b2c0231f66fddc", size = 301083, upload-time = "2026-08-17T08:35:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/ed759787ffe802bd8e31cfcdad3755cbeca2dcdafd2f790cd6f25d195199/xxhash-4.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bfed61996d618eb90d6eaae0178002e3466a28b06bfc557a7a3a7266378d8c5a", size = 312745, upload-time = "2026-08-17T08:22:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/f64b4a4cc8b51e950709207f55f7f56ae9c5af6631dd31d7fb443312418c/xxhash-4.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9761ff4a0ffa583fe850731ad24fe82c88cccb7a2294727db0955f3279a4cb3f", size = 301419, upload-time = "2026-08-17T08:35:50.143Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/bac313b8de073569b8db3152044a7cfcce87a3fa9698c18fe9f914dee6b1/xxhash-4.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edccc2ec58435a580f96a48a3ccae8cd0a480824119165dd90108718ad81ae6e", size = 534485, upload-time = "2026-08-17T08:22:11.515Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/16b5e419f24e59507ee05626d2bb0deafdb03f9f27783bc0785a9849602e/xxhash-4.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4741d42d59e4e5fa1a86c17ab9c27dc8ea459c700d91b6742fdb9138d9a516cb", size = 279605, upload-time = "2026-08-17T08:22:52.934Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/5787dd6e2d8d5b61256a5039f6b18c2193c7c1de4a2fd2413288d0d9c604/xxhash-4.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:440c401e146ce64bdb3beb8ff0c84677b6f21307c28a34779071cecee5d4d70c", size = 358924, upload-time = "2026-08-17T08:35:58.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/89be41991f3b0a2e91f940bdf3128852c3ed571cf560d98ad0f67024afe4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5b7979f71d06ae45a769de0699900a246d8cb632db1e8bfdc79ec019063a503c", size = 295305, upload-time = "2026-08-17T08:22:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5a/52ff0a0cc361aad393ff9a46ffe3aabbcf9c03d6c8f2612da7d553048276/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:62198213fc3e0c56e567894b318ba45834e007d065f84ba6dc9165d21546fc56", size = 320228, upload-time = "2026-08-17T08:35:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b5/91c60ff22c7f6cd5f6d7a5bad5a2cdcb4c33987dfa50bf13f0d856279b2e/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3bece52127ac20044311ee73567f9f0893b5de64f9028aecc90cc740cfd525a", size = 279414, upload-time = "2026-08-17T08:23:03.212Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/9685954804d47d0390871a64bec606a0d536406382d71a784df3a5883fb4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a865d2d470220e659220fdb59d5b6c4422802d8d6098e1324bc4d12444798914", size = 297594, upload-time = "2026-08-17T08:35:57.881Z" }, + { url = "https://files.pythonhosted.org/packages/89/62/b67ac9412907b7a07a2a0c08c3440b9e4480231a7b3de0767e87011e4564/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8580aab306888224074c7edeec734de0c3c5ccde65b2da4e6c9a5e28f7c0a1bd", size = 348526, upload-time = "2026-08-17T08:22:18.571Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/6723cc49a9f567d52d01fd7c1741b0f2e3a13e71d15f7ac49d753a20c115/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2d52dc7c33c1b83082b707f6b7814dc76d2faaa2ea62bd9c5fab4b36f83c087f", size = 499307, upload-time = "2026-08-17T08:22:56.52Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/7b10e101ab988d93b791023be7191d7661271d6ab31ac082276b9091042a/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a9f98af872355e0c02439e48583958eee00e60b928bb20476460d9d40cb7b4e", size = 274989, upload-time = "2026-08-17T08:36:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/7eabcc8d29cce40621443cff24c07d7306ef574b8956c47ac59f21098005/xxhash-4.0.1-cp314-cp314t-win32.whl", hash = "sha256:a14578102a6081465aec9cf73c76c3cd3f79f0709bdb3b8ae7ab0b54c9d8b089", size = 35482, upload-time = "2026-08-17T08:22:32.336Z" }, + { url = "https://files.pythonhosted.org/packages/ca/89/2a4268e1971f63038b79fb75e3b9c8de942cd77acabbb0c5625352a31940/xxhash-4.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c57963970d359a72262f7fe6be88f945e2334d4bc41462b7f08c37b0abf35ca6", size = 37490, upload-time = "2026-08-17T08:35:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/90/7b/950ecab1fe4cf421d0a6211ddd9a0ac82e39e55c45a111ceb90953dc6c9a/xxhash-4.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:b659fad79c99b0238c7ad7e9d7dbf4eebfea9097c2dba65fa0a4d18a25b29a2f", size = 34596, upload-time = "2026-08-17T08:23:10.001Z" }, + { url = "https://files.pythonhosted.org/packages/c4/03/7dc3b85fac10751613bfedb0e120734e0e8710054abad3f931e9d3843a14/xxhash-4.0.1-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:5adf927dca8c47fde7e683fe69efdd81bc865c4db1fb6bb00b391e2b6185207b", size = 43749, upload-time = "2026-08-17T08:36:00.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/bfac071c5b1c6d6a3d48ab1ab96a15e958a1d7061f4afc97804292d87264/xxhash-4.0.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:c30dd1af66a820820398b26e0d74e7a9aa43cae705924f23ed828cd8e5c26c3d", size = 40758, upload-time = "2026-08-17T08:22:30.209Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/49a260e685d1a74c56a69432a8ee0527ddcbd684a3c51f87edc3b75639c5/xxhash-4.0.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1bc591533fc975614f7e13594daee76af96b8e1fbcf8de76c8773858fa9e7cea", size = 34788, upload-time = "2026-08-17T08:23:09.014Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/50d72ed2170dae872e1c0fe333d0908e0a2afbffe74c5c9037d5406a4b89/xxhash-4.0.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:567cbc630302a46a8ecfd943b309ccf5372bb3718f1f3762d452df30f033bcf0", size = 35746, upload-time = "2026-08-17T08:36:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/66/f0/969deaa2bab3bfd5ad5b023442124d2255b9961eef6f797ec74eb8683bdf/xxhash-4.0.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e998cb3685b92101ec5de0fb4d9485cf01e50bc418211955c55d98064664cf4c", size = 38098, upload-time = "2026-08-17T08:22:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/86/aa/45ed7d7b8d7b66202a47bf8ff3b77cea28d2ea54dfcdd202b4cfe043e3dc/xxhash-4.0.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c3074db513c81f764053e3da079312ecf85a50d8350c71f4cc0105d9662a9e6c", size = 38251, upload-time = "2026-08-17T08:35:25.774Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9d/45e7520a7856e13800a5dc8cd038d34c6372429465b163af0c5722f16918/xxhash-4.0.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3088dadbffa33c29e0518578430a7dff2e901a212e487aefa5faaa0dc06dad34", size = 35986, upload-time = "2026-08-17T08:23:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/5ad466e5fea18c9f9bdc5828c0506f62190061b4a1b0e688aa54969d0a9e/xxhash-4.0.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b50223d92df94d54e1a31469335a2c74b16692e6c1cb726f1e6949514458706", size = 264609, upload-time = "2026-08-17T08:36:04.229Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/8f269f85217e3dbd45e31e25e46cc26f3aff0e159ef05d228b4b982c778c/xxhash-4.0.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:427b62d62d4f967fbb10b82a3813e4875c2a6e7e7634739f17265b650c7f65a6", size = 285910, upload-time = "2026-08-17T08:22:38.589Z" }, + { url = "https://files.pythonhosted.org/packages/ca/30/2fc1a16ee0f9501d074b798ebfae52e24fa602c7117f5c4b81de71eada72/xxhash-4.0.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c6370189e8e66b7e608f533b939a9de092ddca6cce084ca0d3d414d2ed5b5d59", size = 306566, upload-time = "2026-08-17T08:23:16.895Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a7/08375cf2b997e1903663fe7525c5973b1987a4f8ad2b8d47463e9143f2ee/xxhash-4.0.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec1a470c6db94ac4589c203921e89ac1bc13e796a8b1784d8135e1893559cd3b", size = 287978, upload-time = "2026-08-17T08:36:09.296Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/90a7b404c11add9e53a497d06236152852490c3b2f21e468d97a58f26afe/xxhash-4.0.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f667dee0f867c42894b34e2a6fe26bf195c0ea4683d9d2b713db023f242c3a", size = 520098, upload-time = "2026-08-17T08:22:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/11/02/7fba10b1b17eb46308f09cc0a4ed513d74dff16b1e22a1c439f011c77129/xxhash-4.0.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f18732adcc271741bd651c3e56fa519d8a237d2cccda01fe3afb226bf87f783b", size = 268273, upload-time = "2026-08-17T08:35:29.043Z" }, + { url = "https://files.pythonhosted.org/packages/54/49/c21b228877357a3be43eeeaa22182ad1685796f415390ada475922c084e4/xxhash-4.0.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b42a5a26607e4b2409fea174773a66f2dff9dfdbf2c1a851bb7b804e2c97535", size = 350164, upload-time = "2026-08-17T08:23:29.494Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/c15bb4aa33d94b78a5553b52e7fa1070565f0199925aeadec3871de20ce9/xxhash-4.0.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:99166cc98637e8bf550cda2aab07f4f1d5f899c45fbd721801aeabcc9d404824", size = 281975, upload-time = "2026-08-17T08:36:08.139Z" }, + { url = "https://files.pythonhosted.org/packages/18/7a/b1d0388315fe7752b7725b68a912667526a1dd48ed492fcc031ac03f4b52/xxhash-4.0.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6cf633df84d80a1668fcf61e330791dae46825e395549e7d34f376411e75088a", size = 307872, upload-time = "2026-08-17T08:22:42.206Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a1/037cb2dd8cf725c9565dfe3712b2915c0e0276a9154913dbfcbcecbeb672/xxhash-4.0.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:e259bb7e1e2d8de6b35f430f5c7220b1c0ebf3962d1ba7ec7545980d5931edb8", size = 268241, upload-time = "2026-08-17T08:23:23.997Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/67c44422d0ee082169b238ce24bd2796b82d7c21ed953471365df8c508d8/xxhash-4.0.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:704381264b36a18b9c62ecbabe2e71d0fc58c77c129c15355c989b10bf05b6b0", size = 284970, upload-time = "2026-08-17T08:36:13.476Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/254a5f51c4014cacc77a26f321372338b924f54e89efb730164ee336d850/xxhash-4.0.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:e90b4bcf1d9eb1010fdaee7c9209fb667e74c0684f3ba17f9032bd7319da90c9", size = 338074, upload-time = "2026-08-17T08:22:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/64/03/f21c4830118d72ef3a958ce8bf2152f49e0d4cf200907616c9be6caf372a/xxhash-4.0.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:a65785e653573fcd1e33062760ab4c3c3440e8e910765018e4b6ed4ad07b54a0", size = 487626, upload-time = "2026-08-17T08:35:32.768Z" }, + { url = "https://files.pythonhosted.org/packages/45/1f/268a689d741d7da649317eb4ce41760140beb4179aaf43a7216fdbe8100c/xxhash-4.0.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e3996ff9b6f99180357024336bf5749a8ad6476a9a2523e535c5212b995b12a2", size = 263852, upload-time = "2026-08-17T08:23:41.871Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/adaf8101cd7f143191a0b390600294d83924b32cb13770fde8803dce27a2/xxhash-4.0.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:99054b838b74d8d3995ea0d410976ae967c46207ae22d6ddfc535e809197dab9", size = 20569, upload-time = "2026-08-17T08:36:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2c/56a5eb8c993420fc07114c08f447a2b66ee996510b4764cb368b9b44c9f0/xxhash-4.0.1-cp315-cp315-win32.whl", hash = "sha256:6c45258a37fc22721395c09927cb982d3e7a83607cab15be7e2416501bd3a330", size = 35145, upload-time = "2026-08-17T08:22:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/67/c7/65f210db43e62157d0fef3b4d4d7b394821e7733c8bb4ece49f91410a725/xxhash-4.0.1-cp315-cp315-win_amd64.whl", hash = "sha256:0ab851b45c70d4992be7cdeeee16f97a0b677408c758c4b1efb1cfe8030bfd37", size = 37161, upload-time = "2026-08-17T08:23:32.438Z" }, + { url = "https://files.pythonhosted.org/packages/33/ae/1a641d1d60ba219756d9ebe907ff0ecf4445adcf4fa96f6e3da57b91d439/xxhash-4.0.1-cp315-cp315-win_arm64.whl", hash = "sha256:a5b21b42a01a343096a1c018d35e9b7aec9c7065dda53ae8da071e37478b2cea", size = 34378, upload-time = "2026-08-17T08:36:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/7f/7698b320b251806d1249e513922a626f19027e104c829a611272250350eb/xxhash-4.0.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:44ab12e8cd17d4f001769f00ad465208b4bcb897ed29e65f058f74466b57a98f", size = 38610, upload-time = "2026-08-17T08:22:55.203Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/436497e775b647b3b3e9a4ffe8c76c59fa4aa7a9fab6447cb59acf1b50ea/xxhash-4.0.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45e88111ebe331de478ef8d4293efbe88f3cf8b863386c9a2357136b838e1af0", size = 36378, upload-time = "2026-08-17T08:35:36.18Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d8/17a4f8182b9257898aa2a77c2a45f70233eb8e50681a280e8e09d2ee76e9/xxhash-4.0.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf430c587f447a554c53768ad76b9846fe7c5632180ef6f69c4fce8b0552fbd0", size = 273559, upload-time = "2026-08-17T08:23:51.075Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/121bd5a5c5adb88e0da772c7bef61964cf9da92956a7a237c7d24c4351b8/xxhash-4.0.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adbd48b30e3f82c89fb2b3e6a87cdd28d113b190a5ed0ee2dee286323ee9a621", size = 299133, upload-time = "2026-08-17T08:36:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/57c7b6e04642ed738a0d08a31bed7fc63fdacb661d665f98739cc9751b62/xxhash-4.0.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e71b34978e77868cbf2d18c5206a4603f9c644dd7181bec5643bd40141d3b8c5", size = 314527, upload-time = "2026-08-17T08:22:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/8e/18/42793917dbab0ea1ff71458aea4875e17a7263f2797b798af048dc81e867/xxhash-4.0.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:488ca5c5e28ef56ec4bbb12f835b3f1cbecc5f3510062e70117bc6594851932a", size = 299199, upload-time = "2026-08-17T08:23:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/51dc92443923d8e908d5614f1145d8d696450f9d6c8f1abe243c6f2a0222/xxhash-4.0.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:421b94f3ba7067958d02e38960d987756347aa150df06df11aa68ae1af78c619", size = 531967, upload-time = "2026-08-17T08:36:18.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/c5/d0de77de09661fac71742c4155b1cd65e274f7cc277819d702b6c8ff2db5/xxhash-4.0.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f33cf0baa91eccd2cb7b62bf00f10c2264ef578b71dd33a12962e71a36eb4d32", size = 278764, upload-time = "2026-08-17T08:22:58.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/9a/589929c655aba1bfb2c41ee03e50eec1547c39c3042a66bda9c173a9614b/xxhash-4.0.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23a4376b4a3183cb50d4d2a3179f887a7773cc695eb2c908e551bec3221b8c60", size = 359876, upload-time = "2026-08-17T08:35:40.35Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a8/c1d8c94d54d91db2215565f4b4151c1593af3e6d27ac4c00fd1e8d714a02/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:38c3d22129a6958846a3098d68bc8e661704461c0be4793ae28836e4690c8478", size = 293548, upload-time = "2026-08-17T08:23:54.951Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/85d8abca94508a4dd10561d9dea3e6e68843c6986dd6d9c1b3729c8622e4/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:87cbdec1a7dd930079671a60b249f3ca4e773e6fbd0676e21e36fdc9dd0f3b00", size = 318780, upload-time = "2026-08-17T08:36:17.623Z" }, + { url = "https://files.pythonhosted.org/packages/1b/16/2b920ed456b9cdcfc99ddc20c3afe42f9f807ee5850773c12fd891f3c08d/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:6cbf4e21ef0890804b5bb9ad25c48f9c127758d7f6c66bef374efcacc63c738a", size = 277582, upload-time = "2026-08-17T08:22:57.156Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cc/5811b5997aebb8452047f5800d32fc50eaa29d0ba08d4e426f84450b9c2f/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c101180495cb4ba3617b279a944345c53a5e73b0c150053d1fa8d8af32de9579", size = 295116, upload-time = "2026-08-17T08:23:40.868Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dc/c2f3f9c2f4d6aadb79f17a9f1c9a7ee82638cc873680da044cf29537d2ee/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c0e6ccc2b19ec8a726b2e26062ac71ea63e15500d6bf85910e42481844fdffc1", size = 347065, upload-time = "2026-08-17T08:36:21.618Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4c/750cc642c92252e10772ec09e1a1d995581ba4c3ceb24f6e2d57c7ce47ca/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:8bcba9456242ebf180a04d9443812fd85ffe6bd12bda464dd116fcece8886ff3", size = 497208, upload-time = "2026-08-17T08:23:17.88Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2d/58693cb13d6395f39b6b9bb40c5e0db53a5df7c9fce805aa7e792f64a1a5/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:83b8c2013edb5dc1f9e7268b6496130705bc48d79c86bb8817b3d210b81a5513", size = 274338, upload-time = "2026-08-17T08:35:44.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/08/9aa9787586d9b3e92d63343ce7dc24f0f445fd9e74ff5d6e85dd82233df5/xxhash-4.0.1-cp315-cp315t-win32.whl", hash = "sha256:aa6ccc7f31018484d652cf52db020003433f3c9fa83189c028bd807d2adde503", size = 35471, upload-time = "2026-08-17T08:24:05.795Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ab/4615789c333bee331ac417885c50105715eeb8244bfc68d2bc37dcfd63ca/xxhash-4.0.1-cp315-cp315t-win_amd64.whl", hash = "sha256:daade8936c4deaaf7b01561324ce438ba4f885d717e9adc62b4d67212ad7d7bd", size = 37488, upload-time = "2026-08-17T08:36:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/49f718beb0c55d0411bc4bd90b50a3fbe5863a0e97a2f4d11682ba13d298/xxhash-4.0.1-cp315-cp315t-win_arm64.whl", hash = "sha256:f00330ac7e24769e2032203f2b01794d670916b0c1799fd261340f1af9499875", size = 34590, upload-time = "2026-08-17T08:23:19.597Z" }, + { url = "https://files.pythonhosted.org/packages/86/79/9127ff42a887a348dc4ce3211cf1a962836887adee6f57078132bfba78b4/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:ff48915bf1871a1f19f74c11834c6329443d306cedc0c05fe7fe617810422a80", size = 31836, upload-time = "2026-08-17T08:36:28.261Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/f238693bfdd642adb59c99683964d46d9947fe721ff44d3bd850ae675407/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a76345f5aceb4ec404918edf9c7f2b5507db864dc0d7455982009ac0890b57b", size = 34453, upload-time = "2026-08-17T08:23:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/796ace33cdfb75c91ba6d11615c3bd436355b9f3103e05865bbee9abce57/xxhash-4.0.1-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d86f9e81f3e84e00131ac7c54caf5119ae4ddd82c09c31cff597c813ce1ee2", size = 38488, upload-time = "2026-08-17T08:23:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/2d549e5d5d7759eaf9ac2d2d2ab81ff60f1bb2b52cdaae8e5ec5c6524354/xxhash-4.0.1-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deca2a30d983d240b8375ec2ee0a4288e72042827fc61df2f7671f8467e4cb2f", size = 38206, upload-time = "2026-08-17T08:36:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/79/98/1ee576b27f78e6107ee4ea8ac03e8a52888dff256e57d560f8282c195563/xxhash-4.0.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:7c343ee174d417a44d0c3355602c0cbbfa52a04d1bbbf1723378c7d2c8f60626", size = 37127, upload-time = "2026-08-17T08:23:42.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c5/8b528f4a394a1eabfd5d8b4271f24fff67fa1aebe58bdd3c64967662e6fe/xxhash-4.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:26fe6238c2d5b11ed5063b9bf4eb290624b004fd074688da6bb079bd564f10d7", size = 36450, upload-time = "2026-08-17T08:35:59.217Z" }, + { url = "https://files.pythonhosted.org/packages/b6/03/909926dcedb64ceee629e18d8dffa758f9d3fdbe01e25530ce2bf0eaf9d0/xxhash-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:e53926e76131a74e79cc0b39fa712c227875f180afc68646bd1e1d8a17e60313", size = 33467, upload-time = "2026-08-17T08:24:12.924Z" }, + { url = "https://files.pythonhosted.org/packages/13/b5/ad50456fbfed07f29169bc3fd34e70cbee306a88c17d1a1141a7638de0ee/xxhash-4.0.1-pp310-pypy310_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0718ad66f4ded2411f8e62bdba549ee71e313a2d26ef5060ca3fdbf29897dd3c", size = 48048, upload-time = "2026-08-17T08:36:30.847Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/83cfe6b591a3067ab3f1339931c91211a8b3864d0ea76ad8184f0c1b0515/xxhash-4.0.1-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2200b98a805351cb3142ae4e1fdcc9e91b5e20f5d30d4862b0b96f92558f4e", size = 42719, upload-time = "2026-08-17T08:23:53.581Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/0f654f2831abba09d502751e38816759086dd92749501b128b7c3676ea87/xxhash-4.0.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea5ecf800b45bdb34afe05a1d0dae1f8ea02a290e50636dccd399063f6b180f8", size = 39477, upload-time = "2026-08-17T08:24:03.735Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d4/375b9e4b719ee70e0a6d9e98e2b60aab09efd59f46c7871a112a2e6b0fcb/xxhash-4.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce6d5cc94a50291d080259a126cbf1e9ba4ac861e6429d2f3cdbb1474f51945d", size = 37244, upload-time = "2026-08-17T08:36:35.452Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4f/e0648288a17d0d1084ca4f7bef206097831988fc86af74aa1dff8f1fbd68/xxhash-4.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:554f87034635bcec47c5d72447bf3db7e02da1bf493a0ada010db28a76f891c6", size = 36333, upload-time = "2026-08-17T08:23:52.913Z" }, + { url = "https://files.pythonhosted.org/packages/22/15/34b7f72e9b5a8bfd7e6178de9e1e342bc3de9f07111a5ae26c00506d9edf/xxhash-4.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3c2445edafc300cc40feb6a25a8356a971c30cd0bf47b5349c2ad74c508343b1", size = 33519, upload-time = "2026-08-17T08:36:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/06/4b/e0af324ccf701bf84a7a060bef11c915d45d9e3c5b9caf5b94d62ecb040b/xxhash-4.0.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bdd16718b63aa3ebd68aabb79021a40e47c81374852d41a306b9453141bbcbee", size = 47995, upload-time = "2026-08-17T08:24:14.335Z" }, + { url = "https://files.pythonhosted.org/packages/59/46/cc7130e6ca6b41ab72eb6a03177b933c7c74145545c99a8610ecc208c449/xxhash-4.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b99ebaf9e816ac5069423b1367ee7e8078fbcebcf62545506bb0608d2f4f468", size = 42725, upload-time = "2026-08-17T08:36:34.144Z" }, + { url = "https://files.pythonhosted.org/packages/1c/24/4f26ff9a7dd0998f6d1036bdddef7ce3e78972a74f7fffa7967e7bc3b7e4/xxhash-4.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f484ed57bb3e4142f9d6439568658c38be5f94b702ba00a1ff32c69783b6c66d", size = 39532, upload-time = "2026-08-17T08:23:57.556Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f3/1ac078fc8fceadcf066469acecacb35d2821cbfaf7d6fc5ac2107c7a314d/xxhash-4.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fac4832b638000106207bc44e44b9616a6a416aaee56c62b01d61f3705e49f58", size = 37252, upload-time = "2026-08-17T08:24:06.652Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ac/cacdda1f0a90441297210bc34cf7e4ac1b7318c8030ebd83bdf6fe82f1db/yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750", size = 135466, upload-time = "2026-07-20T02:04:21.695Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a5/1b2ceace0230e40c52ab1b263148059a43a6303219b996affc68f8381836/yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2", size = 97291, upload-time = "2026-07-20T02:04:24.045Z" }, + { url = "https://files.pythonhosted.org/packages/59/1d/340d1a0db7bbce1f291afc044255ebf4ebbce2b25ab1b3f7d3d069080f5d/yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871", size = 97154, upload-time = "2026-07-20T02:04:25.761Z" }, + { url = "https://files.pythonhosted.org/packages/05/41/25596a33c2fb5098dca8dc3773b04221db64ded0b7f8f09885647d864610/yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0", size = 109196, upload-time = "2026-07-20T02:04:27.543Z" }, + { url = "https://files.pythonhosted.org/packages/f2/df/dd9f2fb8a5c6054fbefd1538d2b9b1127e612d2ee64b307a070173b57afd/yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e", size = 102556, upload-time = "2026-07-20T02:04:29.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/57/4754b9d2c8945880290ecba0864e8b0441e117bba70534fe819e3645e174/yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2", size = 117965, upload-time = "2026-07-20T02:04:30.845Z" }, + { url = "https://files.pythonhosted.org/packages/74/b5/6a9ece27d2043c3386f902dd078ab35d29ef5126b3206ebffb673283a7cb/yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621", size = 116266, upload-time = "2026-07-20T02:04:32.573Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bc/a6653249f6ee59ec85dcfec008d9cbc16586dad613963bb17a91b2b993a5/yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba", size = 110758, upload-time = "2026-07-20T02:04:34.235Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c5a12fb8208df7b981bc82256e7831ce428eeaf893f7bbe6179c57bb9252/yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950", size = 110120, upload-time = "2026-07-20T02:04:35.85Z" }, + { url = "https://files.pythonhosted.org/packages/04/6c/1b659b964626694667b3ec01bf4bcff564b73ae7c48ea1fbfe588b78b461/yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00", size = 108834, upload-time = "2026-07-20T02:04:37.67Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/bf48f55c2104e40c15b7b13fad0a5756a11552a55f01c90bc90a66ab81c3/yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed", size = 103442, upload-time = "2026-07-20T02:04:39.576Z" }, + { url = "https://files.pythonhosted.org/packages/37/ac/84b273ac133ecdce598fc1f4140a08a1bf2044048bff8106371d207d105f/yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440", size = 117413, upload-time = "2026-07-20T02:04:41.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/55/9307e03977d3b290dfa42e5d2bae7b6140808fd1786fbe70cd9d3bee53c5/yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1", size = 109498, upload-time = "2026-07-20T02:04:43.468Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/791a6f314cb4c989c19f8e3a10271f1e469c077143915e52474d80f26b4b/yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6", size = 116062, upload-time = "2026-07-20T02:04:45.319Z" }, + { url = "https://files.pythonhosted.org/packages/19/1a/ddd3807b86055010e2f99aa89b3c640effdb65696766c20597f696f48a1c/yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d", size = 110941, upload-time = "2026-07-20T02:04:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/6d/03/f34271bba042d2187508bf62aea20a14129efb5a1acfc6a2efe7544630b4/yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224", size = 97534, upload-time = "2026-07-20T02:04:48.774Z" }, + { url = "https://files.pythonhosted.org/packages/e4/02/ecc8dc31b9f355731e700f8402b8075d2ea1737dbc4baf4abf0f0fc64288/yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13", size = 93603, upload-time = "2026-07-20T02:04:50.686Z" }, + { url = "https://files.pythonhosted.org/packages/fe/db/3cb5df059756a45761cc3dee8fd25ec82b83a6585ea3542b969fda850f99/yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3", size = 135043, upload-time = "2026-07-20T02:04:52.39Z" }, + { url = "https://files.pythonhosted.org/packages/44/f8/767d6bd5a03db63bc467df2fb56d6fafeae9667d74aea92cd6af399f828b/yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a", size = 96942, upload-time = "2026-07-20T02:04:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/ce/97/10b939c44d7b28d1dbc389cfc7012306d1ea8dba01eaef44b39fffaee52a/yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840", size = 97046, upload-time = "2026-07-20T02:04:56.638Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7a/b410dbe39b6255c55fb2a2bcee96eb844d0789235ddc381a889a90dc72d6/yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966", size = 110512, upload-time = "2026-07-20T02:04:58.955Z" }, + { url = "https://files.pythonhosted.org/packages/83/c7/da591971f78a5617e1f21f5699858ebccd836fe181a6493788ffc91ba69b/yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723", size = 102454, upload-time = "2026-07-20T02:05:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8e/73b0ed4de47289a78a96045d76d1cfe5e41848bf0da59ce25b2ec87ee05d/yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb", size = 117617, upload-time = "2026-07-20T02:05:02.325Z" }, + { url = "https://files.pythonhosted.org/packages/cf/14/b744747bc4f57a8d55bd744df463457524583e1e9f7538b5ace0346ab92e/yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780", size = 116135, upload-time = "2026-07-20T02:05:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/66/ca/95aa4d0e5b7ea4f20e4d577c42d001ed9df207569fdb063cc5ed4ebb496b/yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e", size = 111935, upload-time = "2026-07-20T02:05:05.738Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/d2ad8d6b147832d177a4e720ba1962fe686eb0913b74503b3eca094b8bba/yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2", size = 110010, upload-time = "2026-07-20T02:05:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/50/18/eb335e4120903903f4865041355ae46256a2406eb2865bc24827f4f27b61/yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58", size = 110058, upload-time = "2026-07-20T02:05:09.246Z" }, + { url = "https://files.pythonhosted.org/packages/44/70/97353add32c62ad6f206d948ac5a5ee84398225e534dc6ed6433d1b335b6/yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61", size = 103308, upload-time = "2026-07-20T02:05:11.31Z" }, + { url = "https://files.pythonhosted.org/packages/68/39/5e7398d4b6f6b3c9062823ebc60802df5b272e3fe9e788f9734c6ee46c85/yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6", size = 116898, upload-time = "2026-07-20T02:05:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/09e52f2239e8b96357eccca05915382e4ba5405ebfb623b6036040d99654/yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f", size = 109400, upload-time = "2026-07-20T02:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6a/e94133d4c2d1a14d2384310bf3e79d9cf32c9d1eae1c6f034fb80d098fa1/yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077", size = 115934, upload-time = "2026-07-20T02:05:17.78Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/34955ed967b976fc38edcbb6d538dee79dbda4cb7fc7f72a0907a7c78e0f/yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd", size = 112178, upload-time = "2026-07-20T02:05:19.675Z" }, + { url = "https://files.pythonhosted.org/packages/f5/46/d7bd3a8859d47dcfaffd7127af7076032a7da278a9a02e17b5f37bfb6712/yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25", size = 97544, upload-time = "2026-07-20T02:05:21.523Z" }, + { url = "https://files.pythonhosted.org/packages/01/69/c1bfd21e32c638974ea2c542a0b8c53ef1fa9eff336020f5d014f9503ff2/yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a", size = 93359, upload-time = "2026-07-20T02:05:23.493Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, + { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" }, + { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" }, + { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" }, + { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" }, + { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" }, + { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +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" }, +] diff --git a/src/hooks/fp-reset.ts b/src/hooks/fp-reset.ts index cf7f67aa5..47df6ddec 100644 --- a/src/hooks/fp-reset.ts +++ b/src/hooks/fp-reset.ts @@ -1344,8 +1344,12 @@ function staleDaemonHint(): string[] { `[failproofai] daemon is ${skew.installed}, CLI is ${skew.expected}.`, `This machine is configured to REQUIRE the daemon. A daemon built against a`, `different on-disk layout refuses to start, and this version moved it — so the`, - `next reboot or restart can leave the service down, which denies every tool`, - `call until it is fixed.`, + // The wrap is load-bearing: "denies every tool call" is the consequence + // this message exists to state, and splitting it across two lines is + // what made the test asserting that phrase fail while the text looked + // perfectly correct to a human reading it. + `next reboot or restart can leave the service down, which`, + `denies every tool call until it is fixed.`, `Run \`failproofai update\` now to bring the daemon in line.`, ``, ];