diff --git a/README.md b/README.md index 1b5f00b..f82389d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,18 @@ Use it when you want one reproducible loop: 4. Promote the result into a replayable artifact. 5. Prove release readiness with local gates. +### The harness: point it at an agent and talk to it + +`src/fi/alk/harness/` builds all of the above **for** an agent instead of asking you to write it. +Point it at an agent's source and it reads what that agent verifiably is, builds a real world its +tools act on, and writes test scenarios that are each proved before they are kept. It is driven +as a conversation, in a terminal or on a web page. + +- **[Start here](src/fi/alk/harness/README.md)**: setup from nothing, then how to use it +- **[The web page](harness-ui/README.md)**: the same harness as a chat, on `localhost:8777` +- **[How it works](src/fi/alk/harness/HOW-IT-WORKS.md)** and + **[why it is shaped this way](src/fi/alk/harness/DESIGN.md)** + OpenEnv/Gymnasium shapes are compatibility inputs, not the product center. Agent Learning Kit is the primary runtime and release contract, and the bar is the executable `environment_10x_robustness` release gate. diff --git a/harness/.dockerignore b/harness/.dockerignore new file mode 100644 index 0000000..4145800 --- /dev/null +++ b/harness/.dockerignore @@ -0,0 +1,34 @@ +# Credentials, first and deliberately. Nothing in the Dockerfile copies these today, but the +# whole context is handed to the daemon, and the day someone adds a broad COPY is the day live +# keys land in an image layer. Excluded here so that mistake cannot be made. +.env +.env.* +*.pem +*.key +*-credentials.json +service-account*.json + +.git +.github +.venv +venv +__pycache__ +*.pyc +.pytest_cache +.ruff_cache +.mypy_cache + +# Session artifacts: contracts, worlds, transcripts, recordings. Written at run time into a +# mounted volume, and large enough to slow every build if they were sent as context. +artifacts +sessions + +node_modules +dist +build +*.egg-info + +docs +examples +typescript +oss diff --git a/harness/.gitignore b/harness/.gitignore new file mode 100644 index 0000000..b07bc61 --- /dev/null +++ b/harness/.gitignore @@ -0,0 +1,6 @@ +.venv/ +.pytest_cache/ +__pycache__/ +*.py[cod] +artifacts/* +!artifacts/.gitkeep diff --git a/harness/Dockerfile b/harness/Dockerfile new file mode 100644 index 0000000..0e950d8 --- /dev/null +++ b/harness/Dockerfile @@ -0,0 +1,52 @@ +# The harness in a container. +# +# Three runtimes have to be here, and each is here for a reason worth stating. Python runs the +# harness itself. Node runs the Claude Code CLI, which the Agent SDK drives as a subprocess -- +# without it every stage fails at its first model call. The docker client is here because the +# harness starts containers of its own: a store for a world that needs a real engine, and an +# agent whose tools cannot be imported. It talks to a daemon over the socket proxy rather than +# running one of its own, so there is no Docker-in-Docker and no privileged mode. + +FROM node:22-bookworm-slim + +ENV PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PATH="/opt/venv/bin:${PATH}" + +# Bookworm's python3 is 3.11, which satisfies requires-python. The venv is not isolation from +# anything here; it is what lets pip write at all, since a Debian-managed interpreter refuses. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl git python3 python3-venv \ + && rm -rf /var/lib/apt/lists/* \ + && python3 -m venv /opt/venv + +# The client alone, not the daemon: the static tarball is a few megabytes where the distribution +# package would pull in an engine this container must never run. +ARG DOCKER_CLI_VERSION=27.3.1 +RUN curl -fsSL "https://download.docker.com/linux/static/stable/$(uname -m)/docker-${DOCKER_CLI_VERSION}.tgz" \ + | tar -xz -C /tmp docker/docker \ + && mv /tmp/docker/docker /usr/local/bin/docker \ + && rm -rf /tmp/docker + +RUN npm install -g @anthropic-ai/claude-code + +WORKDIR /app + +# The harness has its own manifest but imports the parent checkout's public fi.simulate API. +# Build from the parent checkout: docker build -f harness/Dockerfile . +COPY pyproject.toml README.md ./ +COPY src ./src +COPY harness/pyproject.toml harness/README.md ./harness/ +COPY harness/src ./harness/src +RUN pip install -e . && pip install -e ./harness + +COPY harness/ui ./harness/ui + +# Reachable from outside the container, which loopback would not be. +ENV HARNESS_HOST=0.0.0.0 \ + HARNESS_PORT=8777 +EXPOSE 8777 + +CMD ["python", "harness/ui/server.py"] diff --git a/harness/README.md b/harness/README.md new file mode 100644 index 0000000..cff8436 --- /dev/null +++ b/harness/README.md @@ -0,0 +1,390 @@ +# The harness + +Point it at an agent. It reads the agent, builds a real database its tools run against, writes +test scenarios, runs them as conversations, and tells you what held and what did not. + +Nothing here is written for a particular agent. Every stage takes the contract and the world as +input, so a different agent is the same commands with a different name. + +--- + +# Part 1 — Setting up, from nothing + +If you have never run this before, do these five steps in order. They take about ten minutes, +most of which is waiting for the install. + +## Before you start + +You need four things on your machine: + +| What | Check it with | If missing | +|---|---|---| +| Python 3.10 or newer | `python3 --version` | install from python.org, or `brew install python` | +| `uv` (the package manager this repo uses) | `uv --version` | `brew install uv` | +| The `claude` command | `claude --version` | `npm install -g @anthropic-ai/claude-code` | +| A Google Cloud service-account key file (`.json`) for Vertex AI | you were given one, or ask | ask whoever set up your GCP access | + +The `claude` command matters: the harness talks to the model through the Claude Agent SDK, and +that SDK runs the `claude` binary under the hood. If it is not installed, every stage fails +immediately with a connection error. + +## Step 1. Get the repo, then enter the harness + +Every command in this document is run from the standalone `harness/` folder: + +```bash +git clone https://github.com/future-agi/agent-learning-kit +cd agent-learning-kit +cd harness +``` + +Wherever you cloned it, this directory is the one containing the harness `pyproject.toml`. Check +you are in the right place: + +```bash +ls pyproject.toml src/agent_harness +``` + +If that errors, you are in the wrong directory. Do not continue until it works. + +## Step 2 — Install the dependencies + +```bash +uv sync +``` + +This reads the harness `pyproject.toml`, installs the editable parent `agent-learning-kit` +package, and creates a folder called `.venv` here. That folder is the "virtual environment": a +private copy of Python with this project's packages in it, so they do not collide with anything +else on your machine. + +It takes a few minutes the first time. You only do this once. + +## Step 3 — Use the virtual environment + +Two ways. **Pick one and stick with it.** + +**Option A — no activation (what this document uses).** Call the Python inside `.venv` directly: + +```bash +.venv/bin/python -m agent_harness +``` + +Nothing to remember, nothing to undo, works in a fresh terminal every time. Every command below +is written this way. + +**Option B — activate it.** If you prefer typing plain `python`: + +```bash +source .venv/bin/activate # your prompt now shows (agent-harness) +python -m agent_harness # plain "python" now means the one in .venv +deactivate # when you are done +``` + +Activation only lasts for that terminal window. Open a new tab and you must activate again. If a +command ever fails with `No module named fi`, you almost certainly forgot. + +## Step 4 — Credentials + +The harness reaches the model through Vertex AI, which needs your Google Cloud service-account +key. Nothing is hardcoded and no key is ever read from source. + +Create a local env file from the template that ships with the repo: + +```bash +cp ../oss/simulation-acceptance/.env.example .env.acceptance +``` + +Open `.env.acceptance` in an editor and fill in two lines: + +```bash +GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/your-service-account.json +GOOGLE_CLOUD_PROJECT=your-gcp-project-id +``` + +`.env.acceptance` is git-ignored. It holds a path to a private key: **never commit it, never +paste its contents into Slack or a PR.** + +Now load it into your terminal, and pick a model: + +```bash +set -a; . ./.env.acceptance; set +a +export CLOUD_ML_REGION=global +export ALK_HARNESS_MODEL=claude-sonnet-4-6 +``` + +- `set -a; . ./file; set +a` means "read this file and export everything in it". The leading + `. ` (dot space) is what runs it in your *current* shell, so the variables stick around. +- `ALK_HARNESS_MODEL` picks the model. **Use `claude-sonnet-4-6` or better.** Haiku is cheaper + but has twice misread an agent's modality, and modality decides how every later test is run. + +These last only for the current terminal window. Every new terminal, run these three lines again. + +## Step 5 — Check it works + +```bash +uv run pytest tests -q +``` + +These are offline tests: no model calls, no credentials, no network. If they pass, your +install is fine. If they fail, the problem is Step 2, not your credentials. + +Then check the credentials separately, with the cheapest thing that talks to the model: + +```bash +.venv/bin/python -m agent_harness +``` + +Say hello. If it answers, the credentials work; type `q` to leave before it spends anything +real. + +--- + +# Part 2 — Using it + +## The short version + +```bash +cd path/to/agent-learning-kit/harness +set -a; . ./.env.acceptance; set +a +export CLOUD_ML_REGION=global ALK_HARNESS_MODEL=claude-sonnet-4-6 + +uv run python ui/server.py # a web page, on :8777 +uv run agent-harness # the same thing in the terminal +``` + +Either one is the whole interface. Both open with "which agent would you like to test, and where +is it?", and everything after that is a conversation. It finds the agent, reads it, builds the +world, writes the scenarios, and runs them, moving on as each stage produces its artifact. + +**The page is the one to start with**: it shows what each stage produced while you talk, and it +is the same harness underneath. There is nothing separate to build or serve; see +`ui/README.md`. + +One message is enough to begin: + +``` +i want to test my voice ordering agent. the code is at /absolute/path/to/the/agent +``` + +In the terminal version: type what you want and press enter, press enter on an **empty** line to +move to the next stage, and type `q` to leave. + +## Where things are written + +One conversation, one folder. Everything about testing one agent lives together, so closing the +page, restarting the server or coming back tomorrow all resume by reading the folder. + +``` +artifacts/sessions// + session.json which agent, where its source is, when it started + chat.jsonl the conversation itself + contract.json what the agent verifiably is + world.sqlite the world, with handlers/, simulator_prompt.md, sub_goals.json + scenarios// one folder per scenario + runs.json what happened when they ran +``` + +The id is readable and unique (`drive-thru-aaea25`), so two attempts at the same agent are two +sessions rather than one overwriting the other. To start from nothing: +`rm -rf artifacts/sessions/* artifacts/.open-session`. + +## The same stages, one at a time + +Useful when you want to redo one thing without walking the whole conversation. Each of these +stays open for corrections until you type `q`; add `--once` to run it unattended and exit. + +```bash +# read an agent's source and write down what it verifiably is +uv run agent-harness understand --name my_agent --path ../../my-agent-repo + +# build the environment: the world, the simulator prompt, the sub-goal catalogue +uv run agent-harness build --name my_agent + +# write the test scenarios, each proved before it is kept +uv run agent-harness scenarios --name my_agent --count 10 + +# run them against the world here, and grade +uv run agent-harness run --name my_agent + +# or run them against the real hosted agent, as a conversation +uv run agent-harness live --name my_agent +``` + +`--name` is just a label for the folder your artifacts go in. `--path` is where the agent's code +lives — a path to another repo on your disk. + +Useful extras: + +- `run --only [ ...]` runs a single scenario instead of all of them +- `run --quiet` hides the conversation and prints only verdicts +- `scenarios` without `--count` uses however many already exist, because coming back to change + one is not a request for a different number of them + +## What each stage does + +**understand** reads the agent's source and produces `contract.json`: its tools, the exact +argument names and permitted values, its hard rules, its real data. Everything downstream is +confined to this, which is what stops later stages inventing tools or menu items. Anything +changed later goes through an amendment tool and is recorded with its reason, so what came from +the agent and what came from us stay distinguishable. + +**build** produces everything common to every test of this agent: + +- **the world** — a real database behind the agent's tools, with one handler per tool that can + genuinely refuse: a nonexistent id, an unavailable item, an argument outside what the tool + accepts. A refusal is the world working; a crash is a defect, and the two are never confused. +- **the simulator prompt** — for a conversational agent, the person on the other side, written + once with `{{ slot }}` variables each scenario fills. +- **the sub-goal catalogue** — the named things this agent can be checked on, each carrying its + check **as code** wherever the answer is observable, and marked judged only where nothing is. + +It is exercised before it can be saved — every tool probed with a valid call, a bogus id and a +missing argument, plus declared sequences where state must carry across calls — and `save_world` +refuses a world that fails, has no sequences, no sub-goals, only judged sub-goals, no simulator +prompt for a conversational agent, or rows left over from its own testing. + +**scenarios** writes each test as a change on that base. Each one owns a folder, and the code in +it is code, not strings inside a JSON file: + +``` +scenarios// + scenario.json the instruction, the reference solution, which sub-goals it names + setup.py def setup(world) what this scenario changes first + ready.py def ready(world) is the world ready for it + checks/.py def check(world, calls) one per deterministic sub-goal +``` + +`setup` is code rather than a list of rows because "not necessarily the database alone" cannot be +written as rows. The check files genuinely run on their own: + +```bash +python scenarios//checks/.py path/to/world.sqlite # prints held, or FAILED: ... +``` + +Before a scenario is kept it is **proved** by three gates, all pure code, no model involved: + +1. **ready**: reset → `setup` → `ready`. The world must hold what the scenario presumes. A + scenario about the last five items is only a test of the agent if there really are five; + otherwise the agent fails for something we got wrong and it reads as the agent's fault. +2. **solvable**: then run the reference solution and the checks. They must **pass**, or either + the scenario cannot be passed or a check is wrong. +3. **not vacuous**: then reset, set up again, run **nothing**, and run the checks. They must + **fail**. A check that passes while the agent does nothing grades nothing while reporting a + result. + +Only a scenario clearing all three is kept. The reference solution is kept with it, and is never +run against the agent under test. + +**run** gives each scenario its own restored copy of the world and grades from what is left +behind: the state of the world plus every tool call with its arguments. `run` converses with the +agent locally, rebuilt from its contract. `live` is the same grading against the **real hosted +agent**: the webhook its own tools call is answered by the world, so a call for something that +is not there is refused rather than mocked into success. + +## How it grades + +Deterministic by default, a judge only as the fallback. + +Every sub-goal with a check in code is settled by running that check against two things the run +left behind: the world afterwards, and the recorded tool calls with their arguments — so "booked +10 PM when 11 PM was asked" is caught without any judgement. Sub-goals marked judged are handed +to a model with three kinds of evidence: what was said, what the agent actually did, and the +state afterwards. An unanswered claim counts as failed, never as passed, and judged results are +always reported as judged rather than blended into the code-settled score. + +``` +PASS quantity_and_unavailable 3/3 sub-goals settled by code + [x] quantity_honored + [x] unavailable_drink_refused + [x] regular_item_placed_correctly + [?] no_unrequested_items — judged, not settled by code + +what the agent actually did: + order_regular_item({'item_id': 'hamburger'}) -> ok + order_regular_item({'item_id': 'hamburger'}) -> ok +``` + +A run where the world crashed is `VOID`, not `FAIL` — that says nothing about the agent. A check +that raises is a **broken check**, reported as ours, never scored against the agent. + +## What it refuses to do + +These are the parts worth understanding, because they are what make a result mean something. + +- A world that fails its own probes will not save; nor will one with no sequences, no sub-goals, + only judged sub-goals, or rows left over from building it. +- A scenario is not kept until the world is ready for it, its own solution passes its own checks, + and those checks fail when nothing is done. Missing preconditions, unsolvable scenarios and + vacuous checks all die here, at write time. +- A scenario naming a sub-goal nobody defined, or a table nobody built, is rejected and told + what does exist. +- A suite where no sub-goal is shared between scenarios will not save, because nothing would + roll up across it. +- Changing the contract is allowed but never silent: every widening, added rule or corrected + tool is recorded with its reason in `amendments[]`. + +If a stage tells you it will not do something, that is the design, not a bug to route around. + +## What a full pass costs, and how long it takes + +Measured on Sonnet, on a five-tool voice agent, all three stages in one conversation: + +| Stage | Turns | Time | Cost | +|---|---|---|---| +| reading the agent | 6 | under a minute | ~$0.55 | +| building the environment | 33 | ~10 minutes | ~$1.40 | +| five proved scenarios | 22 | ~5 minutes | ~$0.92 | + +About **$3.30 and twenty minutes** end to end. Building the environment is the long stage, and +**the Environment tab stays empty until it finishes**: the world is held in memory until +`save_world` writes it. Watch the chat for progress instead. Grading a local run afterwards is a +few cents per scenario. + +## When something goes wrong + +| What you see | What it means | +|---|---| +| `No module named fi` | Run `uv sync` from `harness/`, then use `uv run` | +| `command not found: uv` | `brew install uv` | +| `No module named 'fastapi'` | Run `uv sync` from `harness/` to install the UI dependencies | +| Fails instantly on any model call | The `claude` command is not installed, or your env vars are not loaded in this terminal | +| `Could not load the default credentials` | `GOOGLE_APPLICATION_CREDENTIALS` is unset or points at a file that is not there | +| `nobody has said which agent this is about yet` | Say where the agent's code lives, with an absolute path | +| `No contract at ...` | Read the agent first | +| `No world at ...` | Build the environment first | +| The page shows empty tabs | Look at which session is open. A build in progress has not written its world yet | +| A stage does nothing and exits | It ran out of turns. Look at the last few lines: it usually says what it was stuck on | +| A change to the harness seems to have no effect | Restart the server. A long-lived process does not reload code or skills | +| `lsof -ti:8777` says the server is up after you stopped it | That matches a browser's leftover sockets. Use `lsof -nP -iTCP:8777 -sTCP:LISTEN` | + +Everything a stage did is printed as it happens, and every run is kept in +`artifacts/sessions//runs.json`, including the transcript and every tool call. + +--- + +# Part 3 — For developers + +## Adding to it + +- A new **agent** is nothing: the same stages read its contract. +- A new **kind of world** is a class and a registration in `world/kinds.py`. Browser is registered + and stubbed; sqlite is the one built out. +- A new **place the agent runs** is a class and a registration in `run/targets.py`. `local` runs + the agent here from its contract; the live voice path answers a hosted assistant's webhook from + the same `world.handle_tool_call`, so the world, the scenarios and the grading do not change. +- A change to **how a stage works** is an edit to its `skills//SKILL.md`. The markdown is + the method; code holds only what must be exact. + +## Not done yet + +- Browser worlds are registered but not built. +- Snapshots are local files, not object storage. +- Judged sub-goals on the live path are reported as judged, not yet sent to a judge. +- Nothing reports which of the contract's use cases have no scenario. + +## Tests + +```bash +uv run pytest tests -q # offline, no credentials needed +``` diff --git a/harness/artifacts/.gitkeep b/harness/artifacts/.gitkeep new file mode 100644 index 0000000..a2b5adb --- /dev/null +++ b/harness/artifacts/.gitkeep @@ -0,0 +1 @@ +Runtime harness artifacts are intentionally not tracked. diff --git a/harness/pyproject.toml b/harness/pyproject.toml new file mode 100644 index 0000000..9a7f706 --- /dev/null +++ b/harness/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "agent-harness" +version = "0.1.0" +description = "Standalone voice-agent demo harness for Agent Learning Kit simulations." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "agent-learning-kit[livekit]", + "fastapi>=0.115,<1", + "uvicorn>=0.30,<1", +] + +[project.scripts] +agent-harness = "agent_harness.cli:main" + +[tool.uv.sources] +agent-learning-kit = { path = "..", editable = true } + +[dependency-groups] +dev = ["pytest>=8.3"] + +[tool.hatch.build.targets.wheel] +packages = ["src/agent_harness"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +addopts = "--strict-markers" diff --git a/harness/src/agent_harness/DESIGN.md b/harness/src/agent_harness/DESIGN.md new file mode 100644 index 0000000..8c7c0bc --- /dev/null +++ b/harness/src/agent_harness/DESIGN.md @@ -0,0 +1,246 @@ +# The harness: what it builds and how it proves it + +The reference for the rebuild. Written after the corrections in `_scenario-generation/context/` +7.2, 8.1, 9.1, 10.1 and 12, and it supersedes anything in the code that contradicts it. + +--- + +## The model, in one paragraph + +The **environment step** builds everything that is common to every test of one agent: the world +its tools act on, the prompt that drives a simulated user if it has one, and the catalogue of +sub-goals it can be checked against. Every **scenario** is then only a change on that base: what +it alters after reset, the instruction substituted into the simulator's prompt, and which +sub-goals must hold. Nothing about a scenario is a template with slots; the harness writes each +one, and proves it works before keeping it. + +``` +environment step ─────────────────────────────► base: world + simulator prompt + sub-goals + │ +scenario 1 ──► reset → setup → run → check ───────────────┤ +scenario 2 ──► reset → setup → run → check ───────────────┤ +scenario N ──► reset → setup → run → check ───────────────┘ +``` + +--- + +## 1. The environment step + +> *"You have to first understand what the f\*\*\* this agent is, from that you will create +> databases, you'll create a snapshot of the databases first."* — Nikhil, 12 + +It produces four things. All of them are written by the harness. None are hardcoded here. + +### 1.1 The world + +Whatever **this** agent needs, and nothing more. For the drive-thru agent that is a database. +For a browser agent it is a site. For something else it is a filesystem, a queue, a service — the +harness decides from the contract what has to exist. + +It **subclasses ALK's `EnvironmentAdapter`**, so the runners that already exist can drive it: +`reset` publishes the tools and the starting state, `handle_tool_call` executes one call, and the +state afterwards is what gets graded. Nothing the harness writes should re-implement a runner. + +It is **frozen once** as a snapshot. Every scenario restores from that snapshot, so a run is +repeatable and no scenario can inherit another's leftovers. + +### 1.2 The simulator prompt — only where the agent is conversational + +> *"For voice and chat there is a simulator, and the input is an instruction to that simulator +> rather than an input to the agent under test. Where there is no actor, variability comes from +> how the environment is designed."* — 10.1 §4 + +The harness writes one prompt for the simulated user of **this** agent, with variables left open. +Each scenario supplies the values. The prompt is an artifact of the environment step because it +is the same for every scenario; only the substituted instruction differs. + +There is no persona field and no persona library. + +> *"Drop the gimmicky persona characters. Variability comes from real conditions instead: a new +> versus an existing user, whether a payment method is on file, addresses."* — 8.1 + +For a browser or coding agent there is no simulator at all; the instruction goes to the agent +directly. + +### 1.3 The sub-goal catalogue + +> *"Defining the sub-goals is our call. The important property is that they are common across +> scenarios so the results roll up: if a payment step appears in 50 scenarios, the analytics +> should show where payment fails and how often."* — 10.1 §8 + +Defined **once**, here, as a named list. Scenarios reference them; they do not invent their own +wording. That is what makes `order-confirmation fails in 7 of 12 scenarios` a sentence anyone can +say. Each entry carries its own check (see §3). + +### 1.4 The gate + +The environment is not accepted because it looks right. It is exercised: every tool called with a +valid call, a nonexistent id, and a missing argument; sequences where state has to carry across +calls. **A refusal is the environment working; a crash is a defect.** It cannot be saved dirty +(rows left over from building) or unverified. + +--- + +## 2. A scenario is a change on that base, and it owns a folder + +``` +name identifier, and the name of its folder +use_case which branch of the agent's real use cases this belongs to +setup.py def setup(world), what changes after reset. Code, because what a + scenario changes is not necessarily the database alone +ready.py def ready(world), whether the world holds what this scenario presumes +instruction the task. For a conversational agent this is substituted into the + simulator prompt; for a browser or coding agent it goes to the agent +solution the reference trajectory: what a correct agent would do +checks/*.py one file per deterministic sub-goal, each runnable on its own +``` + +The file is the artifact. Code lives in files rather than as strings inside JSON, and every check +carries a `__main__` block so a person can run it by hand against what a run left behind and get +the same answer the harness got. + +Gone from the old shape: `persona`, `opening`, `goal`, and free-text `must` / `must_not` as the +primary grading. Scenarios are organised **use case → branch**, not by adversarial flavour. + +> *"A login flow is not one row with happy/edge inside it; it is many rows: login-with-Google, +> login-with-Microsoft, forgot-password, sign-up-with-email."* — Nikhil, 7.2 + +--- + +## 3. Checks: deterministic by default, judge as the fallback + +> *"When you have `==` or a python script, then I'll call that deterministic."* +> *"Most likely we can make things deterministic."* +> *"Deterministic, if possible. And LLM also, obviously."* — Nikhil, 10 + +| | | +|---|---| +| **Deterministic** — an assert, an equality, **a python script** | The default | +| **Non-deterministic** — an LLM judging whether a sub-goal was met | Only where nothing observable settles it | + +The trap, in his words: *"you are judging by LLM [so it is non-deterministic], but if you want an +exact output to be 50, then that is deterministic."* An exact fact checked by a judge is **still +non-deterministic**. What matters is who decides, not how precise the fact is. + +A check is code the harness writes, and it has two observable things to work from: + +1. **the world afterwards** — rows, files, whatever this environment is +2. **the recorded tool calls** — that the call happened, *and with the right arguments* + +That second one answers the question left open in 7.2: a booking made for 10 PM when 11 PM was +asked for is a failure, and it is deterministic to detect. + +The judge is left only with what leaves no trace: whether a refusal was explained, whether a price +was invented, tone. + +> Warning from the previous run: *"Judge checkpoints, about a third of all checkpoints, are +> returned as skipped and not graded."* Leaning on the judge does not merely weaken a result — it +> silently produces holes. + +--- + +## 4. Three gates on every scenario, before it is kept + +Terminal-bench's oracle run, which is the reason its tasks are known to be solvable. + +### Gate 1. Ready + +``` +reset → apply setup → run ready ⇒ must HOLD +``` + +The world must hold what the scenario presumes. A scenario about the last five items is only a +test of the agent if there really are five; otherwise the agent fails for a precondition we got +wrong, and the report reads as a finding about the agent. A missing precondition is ours, and this +is where it is caught. + +### Gate 2. Solvable + +``` +reset → apply setup → run the solution → run the checks ⇒ must PASS +``` + +If the checks fail with the reference solution, either the scenario is impossible or the check is +wrong. Both have already happened here: a scenario asserted a value the agent was never permitted +to send, and another demanded confirmation of an item that could not be ordered. This catches +them at write time, with no model involved. + +### Gate 3. Not vacuous + +``` +reset → apply setup → run NOTHING → run the checks ⇒ must FAIL +``` + +A check that passes without the agent doing anything grades nothing while reporting a result. +This is the failure that makes a suite quietly green. + +Vacuity is judged on *all* checks passing, because one check surviving an empty run ("no +unavailable item was ordered") is legitimate. A single check that survives is still named, because +sub-goals are shared: a check that cannot fail without calls would roll up as a pass for an agent +that did nothing at all. + +Neither gate asks a model anything. The environment decides. + +**Three things fall out of the solution for free:** it is the expected trajectory; comparing the +agent's trajectory against it gives efficiency (Nikhil's point about the agent that succeeds on +the 21st call after 20 failures); and a scenario that cannot be run is caught before a call is +ever placed. + +--- + +## 5. Running it + +> *"Use an existing harness. Just for Claude agents. Use any existing harness that is there."* +> — Nikhil, 12 + +The simulation runs through **ALK's own path**, not a loop written here. The world is passed in +as the environment; the agent under test is the real agent, in its real runtime. For the voice +case that means the Vapi assistant we already have, over LiveKit, with the tool webhook answered +by **our world** rather than by canned mocks. + +That last part is the whole point of the environment. The previous run's known issues were: + +- *"Mocked tools always succeed, including removing an item that was never added."* +- *"Mock responses do not vary by argument, so read-after-write flows are wrong."* +- *"World state does not change unless a scenario sets `state_updates`, which is often empty."* + +A world that really holds rows and can really refuse removes all three. + +--- + +## 6. What changes per kind of agent, and what does not + +| | Voice / chat | Browser | Coding | +|---|---|---|---| +| World | database, KB | a site | a filesystem, a repo | +| Simulated user | yes — prompt written by the harness | none | none | +| Instruction goes to | the simulator | the agent | the agent | +| Solution | tool calls | actions | commands | +| Check | code over world + calls | code over the page + actions | code over the tree | + +**What never changes:** the environment is built once and frozen; a scenario is a change on it; a +solution proves it is solvable; a scenario whose world is not ready is rejected before it can be +blamed on the agent; a check that cannot fail is rejected; deterministic first. + +--- + +## 7. Order of work + +1. **Environment step** — the world, the simulator prompt, the sub-goal catalogue, all written by + the harness rather than by a fixed schema here. +2. **Scenario shape**: a folder per scenario: setup / ready / instruction / solution / sub-goal + references / a file per check. +3. **The three gates**: ready, solvable, and not vacuous. +4. **Run through ALK** — the world serving the tool calls of the real agent. + +--- + +## 8. Instructions, not code + +> *"This is a flow, this is not a harness. You will give your harness the instructions that you +> are supposed to do all this and then the harness will do all that. It's not a code that your +> harness follows."* — Nikhil, 12 + +Every stage's method lives in a `SKILL.md`, editable without touching code. What stays in code is +only what must be exact: executing a call, restoring a snapshot, running a check, and refusing +something that does not hold up. **The harness decides what to do. Code decides what is true.** diff --git a/harness/src/agent_harness/HOW-IT-WORKS.md b/harness/src/agent_harness/HOW-IT-WORKS.md new file mode 100644 index 0000000..d8b3555 --- /dev/null +++ b/harness/src/agent_harness/HOW-IT-WORKS.md @@ -0,0 +1,296 @@ +# How the harness actually works + +What happens between you typing a sentence and a graded result appearing. Written to be read +alongside the code, so every claim below names the file it lives in. + +The shape is the same at every stage, and worth holding onto: + +> **A stage is a model session with a small set of tools and its instructions in a markdown file. +> The model decides what to do; the tools do anything that must be exact and refuse anything that +> must not happen. Nothing reaches disk except through a tool that checked it first.** + +There is no pipeline. Each stage is a conversation you can interrupt, correct, and resume. + +--- + +## The pieces + +| Piece | Where | What it is | +|---|---|---| +| Stage | `session.py` | A live model session, held open across turns, emitting typed events | +| Instructions | `skills//SKILL.md` | How that stage works, in prose. Editable without touching code | +| Tools | `tools.py`, `world/tools.py`, `scenario_tools.py`, `run/tools.py` | The exact half: they execute, validate, and refuse | +| Artifacts | `artifacts/sessions//` | What each stage leaves behind for the next | +| Conversation | `chat.py` | Holds one agent's journey through the stages | +| Session | `sessions.py` | One conversation, one folder: chat and artifacts together | + +The artifacts, each the input to the next stage: + +``` +contract.json → world.sqlite + handlers/ + simulator_prompt.md + sub_goals.json + → scenarios// (+ scenarios.json as the index) → runs.json +``` + +All of it, plus the conversation that produced it, lives in one folder per session. There is +nothing held in memory that is not also on disk, so closing the page, restarting the server or +coming back tomorrow all resume the same way: by reading the folder. + +--- + +## 1. Reception — which agent is this? + +`reception.py` + +A stage with `Read`, `Glob`, `Grep` and one tool, `point_at_agent`. You say where your agent +lives; it looks, confirms the path exists, picks a short name, and calls that tool. + +It looks from the **workspace root** (the directory holding your repos), not from inside +`agent-learning-kit`, because the agent under test is almost never inside the harness. + +`point_at(name, path, kind)` refuses a path that does not exist, and refuses an unknown kind. +`kind` selects an `AgentSource` from `sources.py` — `repo` (code on disk, gets file tools) or +`spec` (a prompt and tool schema pasted in, gets no file tools). **A new kind of agent is one +registration, not a new code path.** + +--- + +## 2. Understand — what is this agent, verifiably? + +`understand.py`, `skills/understand-agent/SKILL.md`, gate in `tools.py` + +The session gets read-only file tools and one submission tool. It reads the agent's source and +calls `submit_contract`. + +**The contract** (`contract.py`) is the anti-hallucination device for everything downstream: + +| Field | Why it matters | +|---|---| +| `tools[]` — name, `args`, `arg_types`, `arg_values`, description | The agent's action space. `arg_values` are the real permitted values — the menu, the enum, the lookup | +| `hard_constraints[]` | Rules the agent must follow. Told to the agent under test, and graded by the judge | +| `base_environment` | Its real starting data, reproduced row for row | +| `real_use_cases[]` | What it is actually for | +| `notes` | Free-form: whatever else the reader judged worth carrying forward | +| `amendments[]` | Anything **not** read from source — see below | + +**How it is written:** `accept_contract` in `tools.py` validates before anything reaches disk. It +refuses a contract with no tools, no use cases, duplicate tool names, types for arguments that do +not exist, or — the one that mattered most in practice — *every* tool having no arguments, which +means the arguments were read and then not recorded. Problems are returned **into the +conversation**, so the model corrects and resubmits rather than a bad contract landing. + +**How it is changed later** (`amend.py`). The contract is not frozen, but every change is +recorded with a reason in `amendments[]`, so months later you can still tell what came from the +agent and what came from us: + +- `amend_contract` — let an argument accept a value it did not before +- `add_rule` / `drop_rule` — a hard constraint the source did not state, or one misread +- `fix_tool` — correct argument names, types, description, or remove a tool that does not exist + +Each demands a `why`. A contract that can be rewritten invisibly is no longer evidence. + +--- + +## 3. Build — the world its tools run against + +`build.py`, `skills/build-environment/SKILL.md`, tools in `world/tools.py` + +**This is the part that makes the whole thing worth doing.** Not mocked tool responses: a real +SQLite database with real handlers, so a call for something that is not there is *refused*, and +the agent has to cope. + +It builds three things, all shared by every scenario: **the world**, **the simulator prompt** for +a conversational agent, and **the sub-goal catalogue**. The stage has sixteen tools and no file +access at all: + +| Tool | Does | +|---|---| +| `create_schema` | Run the CREATE TABLE statements | +| `seed` | Insert rows — the agent's real catalogue | +| `change_data` | One UPDATE or DELETE, for fixing a row put in wrong | +| `define_handler` | One tool's implementation, **executed the moment it is defined** | +| `run_tool` | Call a defined tool and see what the world does | +| `declare_sequence` / `drop_sequence` | A series of calls whose end state must hold | +| `inspect_world` | Look at what is in the world | +| `amend_contract`, `add_rule`, `drop_rule`, `fix_tool` | Correct the contract | +| `check_world` | Run every probe, report without saving | +| `save_world` | Freeze it — refused unless it holds up | + +**A handler** is Python: `def handle(args, db)`, with `db.query` / `db.one` / `db.execute` and +`ToolError` in scope. Nothing else — no filesystem, no network, because a world that depends on +the outside is not reproducible. It is `exec`'d per call in `world/runtime.py`. + +**The distinction the whole design turns on** (`runtime.py`): + +- `ToolError` — the world saying *no*. The id does not exist; the item is unavailable. **This is + the world working.** +- Any other exception — our bug. + +They are recorded differently and never confused. `_is_refusal` matches `ToolError` by name +across the class hierarchy, because generated handlers often declare their own. + +### The gate: what `check_world` and `save_world` actually run + +`world/probe.py`. Every probe restores the world to a frozen baseline first, so probes cannot +inherit each other's rows. + +| Probe | Asks | +|---|---| +| `happy` | A valid call built from the contract's permitted values. A refusal is acceptable; a crash never is | +| `edge` | A nonexistent id → must refuse, not succeed and not crash. A missing required argument → must refuse | +| `coverage` | Every contract tool has a handler; no handler for a tool the agent lacks; **and each handler actually reads the arguments the contract says it takes** | +| `data` | Every identifier the contract permits exists in the world — catches a whole category left unseeded, which otherwise looks exactly like correct strictness | +| `sequence` | Each declared sequence, run from the frozen world, leaves the state it claims | +| unknown tool | Calling a tool that does not exist must refuse | + +`save_world` refuses on three counts: **score below 0.85**; **no declared sequence** (calls that +each work alone can still forget what the last one did); and **a dirty world** — rows left over +from building, which would otherwise appear in every scenario as somebody else's order already +in the cart. + +Then `world/snapshot.py` writes `world.sqlite`, `handlers/*.py`, `world.py` and `manifest.json`. +**The snapshot is the base state every scenario restores from.** + +--- + +## 4. Scenarios — the conversations worth having + +`scenarios.py`, `skills/write-scenarios/SKILL.md`, tools in `scenario_tools.py` + +A scenario is a **change on the base environment**, and it owns a folder (`folder.py`): + +``` +scenarios// + scenario.json the instruction, the reference solution, which sub-goals it names + setup.py def setup(world) what this scenario changes first + ready.py def ready(world) is the world ready for it + checks/.py def check(world, calls) one per deterministic sub-goal +``` + +`scenarios.json` is an index over those folders, regenerated from them, so anything wanting the +whole suite at a glance has it. + +Code lives in files, never duplicated into JSON, and the file is the artifact: each check file is +written with a `__main__` block so it runs standalone against what a run left behind, and a test +proves the file and the harness give the same answer. + +`setup` is **code** rather than a list of rows: what a scenario changes is not necessarily the +database alone. There is no persona and no opening line. Variability comes from **real +conditions** that live in `setup`, and the base world stays the shared starting point. + +`sub_goals` are **names from the catalogue the environment step defined**, not restated wording. +That is what makes results roll up: the same sub-goal failing in seven of twelve scenarios is one +sentence. + +`solution` is what a correct agent would do. It is never run against the agent — it exists so the +scenario can be proved. + +The writer can **look** (`inspect_world`) and **rehearse** (`try_calls` — run calls against a +throwaway copy and see what state they leave), so a solution is written from what was observed. + +### The gate: three proofs, no model involved + +`prove.py`, called by `submit_scenario` before a scenario is kept: + +| | Run | Must | +|---|---|---| +| **Ready** | reset → `setup` → `ready` | **hold** | +| **Solvable** | reset → setup → **the solution** → the checks | **pass** | +| **Not vacuous** | reset → setup → **nothing** → the checks | **fail** | + +If the first fails, the world does not hold what the scenario presumes, and running it would test +us rather than the agent: the agent would fail for a precondition we got wrong, and it would read +as a finding about the agent. If the second fails, either the scenario cannot be passed or the +check is wrong. If the third passes, the checks grade nothing while reporting a result, which is +the failure that makes a suite quietly green. + +Vacuity is judged on *all* checks passing, because one check surviving an empty run ("no +unavailable item was ordered") is legitimate. A single check that survives is still reported, in +`Proof.weak`: sub-goals are shared, so a check that cannot fail without calls would roll up as a +pass for an agent that did nothing at all. + +`save_scenarios` additionally refuses a suite where no sub-goal is shared by two scenarios, +because nothing would roll up. + +--- + +## 5. Run — put someone in front of it + +`run/` + +The simulation is **not a loop written here**. ALK already owns placing a call, driving the +synthetic user, and producing a transcript; the harness supplies the world, the instruction, and +the grading. Against the real hosted agent that is `run/live.py` and `run/call.py`: + +``` +world + setup ──► webhook ──► public url ──► the assistant's OWN tools repointed + │ + ALK's voice case places the call ──┘ + │ + the world afterwards + the calls ──► the sub-goals' checks +``` + +1. **Restore** the frozen world and apply this scenario's `setup`. Its own copy, so nothing leaks + between scenarios. +2. **Stand up the webhook** (`run/voice.py`) and bind that world to it. A hosted voice agent + executes its tools by calling a webhook, so answering that webhook from `handle_tool_call` is + the entire integration. +3. **Expose it** (`cloudflared`, or `HARNESS_WEBHOOK_URL`), because a hosted agent cannot reach + loopback. +4. **Repoint the assistant.** `pointed_at` copies the agent's **own** tools and changes only + `server.url`. Nothing about the agent is redefined — rebuilding its tools would mean testing an + agent we wrote. +5. **Place the call** through ALK's own voice case, with the scenario's filled simulator prompt + driving the caller. +6. **Grade** from the world afterwards plus the recorded calls, through the same checks the gates + used. A sub-goal marked `judged` is reported as judged, never silently counted. + +`run/alk.py` is the same story for the text path: the world goes in as `environment=` to ALK's +`ChatEnvironment`, which owns the turn loop. + +Running is also a **stage of the conversation**, not only a command (`run/stage.py`, +`skills/run-scenarios/SKILL.md`, tools in `run/tools.py`: `preflight`, `list_scenarios`, +`run_scenario`, `read_results`). The stage exists because reading a failure is judgement: it has +to sort every failure into one of four causes — the agent was wrong, the world wrongly refused, +the check is wrong, or the simulated caller never asked for the thing — and only the first is a +finding about the agent. Each run's record lands in `runs.json` with the instruction, the +per-sub-goal verdicts, every tool call, and the transcript. + +This is what the environment was built for. The previous run's known issues — *"mocked tools +always succeed, including removing an item that was never added"*, *"mock responses do not vary by +argument"*, *"world state does not change unless a scenario sets `state_updates`"* — are all the +same defect, and a world that really holds rows and can really refuse answers all three. + +--- + +## What is exact, and what is judgement + +The split is deliberate and worth defending: + +| Judgement (the model) | Exact (code) | +|---|---| +| Reading unfamiliar source | Whether the contract is structurally usable | +| Designing a schema | Whether a handler crashes or refuses | +| Choosing what is worth testing | Whether an expectation resolves against real tables | +| Whether a claim held | Whether the state matches | + +**The model never decides whether something passed.** It decides what to try. + +--- + +## Where to extend it + +- A new **agent kind** → a class in `sources.py` and one registration +- A new **world kind** (browser, filesystem, queue) → a class in `world/kinds.py` implementing + `values_present` / `mutable_state` / `describe`, and one registration. Browser is registered + and stubbed +- A new **place the agent runs** (Vapi, LiveKit, a hosted endpoint) → a class in `run/targets.py` + with `open` / `say` / `close`, whose tool calls reach the same `world.handle_tool_call` +- A change to **how a stage works** → edit its `SKILL.md`. No code + +## What is not built + +- Browser worlds: registered, not implemented +- Snapshots are local files, not S3 +- Judged sub-goals are reported as judged, not actually sent to a judge yet +- Results do not post to the platform +- Nothing reports which of the contract's use cases have no scenario diff --git a/harness/src/agent_harness/__init__.py b/harness/src/agent_harness/__init__.py new file mode 100644 index 0000000..3f059f1 --- /dev/null +++ b/harness/src/agent_harness/__init__.py @@ -0,0 +1,62 @@ +"""The harness: an agent that builds test environments for other agents. + +It reads an agent, works out what it verifiably is, builds a world its tools can run against, +generates scenarios, runs them, and reads the results back. Each of those is a stage, each stage +is its own session, and stages hand work to each other as artifacts on disk. + +The split that matters: the model does judgement, and code decides outcomes. Reading unfamiliar +source, designing a schema, and choosing what is worth testing are judgement. Executing a tool +call and grading a run are not, and are never delegated to a model. + +Stages are described in files under ``skills/``, so the method is editable without touching +code, and where an agent comes from is a registered source, so a new kind of agent is a class +rather than a new code path. +""" + +from .chat import Conversation, open_conversation +from .config import ( + DEFAULT_MODEL, + artifact_dir, + load_skill, + provider_env, + read_only_session, +) +from .contract import AgentContract, ToolSpec, validate_contract +from .scenario import Scenario, validate_scenario +from .session import Stage, Turn +from .sources import ( + AgentSource, + GitHubSource, + RepoSource, + SpecSource, + register_source, + resolve, + supported, +) +from .understand import open_stage, understand + +__all__ = [ + "AgentContract", + "AgentSource", + "Conversation", + "DEFAULT_MODEL", + "GitHubSource", + "RepoSource", + "Scenario", + "SpecSource", + "Stage", + "ToolSpec", + "Turn", + "artifact_dir", + "load_skill", + "open_conversation", + "open_stage", + "provider_env", + "read_only_session", + "register_source", + "resolve", + "supported", + "understand", + "validate_contract", + "validate_scenario", +] diff --git a/harness/src/agent_harness/__main__.py b/harness/src/agent_harness/__main__.py new file mode 100644 index 0000000..eb53e2f --- /dev/null +++ b/harness/src/agent_harness/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +raise SystemExit(main()) diff --git a/harness/src/agent_harness/amend.py b/harness/src/agent_harness/amend.py new file mode 100644 index 0000000..d7c6c88 --- /dev/null +++ b/harness/src/agent_harness/amend.py @@ -0,0 +1,367 @@ +"""Changing the contract after the fact, and being honest about having done it. + +The contract is what the agent verifiably is, read from its own source. That makes it the thing +everything downstream is confined to, and it is why the harness cannot invent a tool or a value. + +But it is not permanent. Two situations genuinely require changing it, and they are different: + +- **It was read wrong.** Stage one missed a value the agent really accepts. Correcting that is + restoring the truth, and the correction should come from the source. +- **The agent is being changed.** Somebody adds an item to the world because the real menu is + gaining one. The world and the action space have to move together: an item the world holds but + the agent cannot name is dead data, and a scenario about it can only fail. + +Either way the amendment is recorded on the contract itself rather than blended into what was +read, so that a month later it is still possible to tell what came from the agent and what came +from us. That distinction is the whole value of the contract; quietly widening it would make it +the same kind of guess it exists to prevent. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from .contract import MODALITIES, AgentContract, validate_contract + +CONTRACT = "contract.json" + + +def widen( + contract: AgentContract, + destination: Path, + *, + tool_name: str, + argument: str, + values: list[str], + why: str, +) -> tuple[bool, str]: + """Let a tool's argument accept values it did not before. + + Amends the contract the stage is holding, then persists it. Loading a second copy from disk + and writing that back would leave the stage still working from the old one, so the world it + goes on to check would be checked against an action space that no longer matches. + + Returns whether it was amended, and what happened. + """ + spec = next((tool for tool in contract.tools if tool.name == tool_name), None) + if spec is None: + return False, ( + f"{tool_name!r} is not a tool this agent has. It has: " + f"{', '.join(sorted(contract.tool_names()))}" + ) + if argument not in spec.args: + return False, ( + f"{tool_name} takes no argument called {argument!r}. It takes: " + f"{', '.join(spec.args) or 'nothing'}" + ) + if not why.strip(): + return ( + False, + "say why: an unexplained amendment is indistinguishable from a guess", + ) + + existing = spec.arg_values.get(argument) + current = list(existing) if isinstance(existing, (list, tuple)) else [] + fresh = [value for value in values if value and value not in current] + if not fresh: + return False, f"{argument} already accepts {', '.join(values) or 'nothing new'}" + + spec.arg_values[argument] = [*current, *fresh] + contract.amendments.append( + f"{tool_name}.{argument} widened by {', '.join(fresh)}: {why.strip()}" + ) + + problems = validate_contract(contract) + if problems: + spec.arg_values[argument] = current + contract.amendments.pop() + return False, "the amended contract would not be valid: " + "; ".join(problems) + + destination = Path(destination) + destination.mkdir(parents=True, exist_ok=True) + (destination / CONTRACT).write_text( + json.dumps(contract.model_dump(), indent=2, ensure_ascii=False), + encoding="utf-8", + ) + return True, ( + f"{tool_name}.{argument} now accepts {', '.join(fresh)}. " + f"{len(contract.amendments)} amendment(s) recorded on the contract." + ) + + +def add_rule( + contract: AgentContract, destination: Path, *, rule: str, why: str +) -> tuple[bool, str]: + """Give the agent a rule its source did not state. + + A hard constraint is not decoration: the agent under test is told it, and the judge grades + against it. So this is a real change to what is being tested, and like a widened argument it + is recorded rather than blended into what was read from the source. + """ + rule = rule.strip() + if not rule: + return False, "no rule given" + if not why.strip(): + return False, "say why: an unexplained rule is indistinguishable from a guess" + if any(rule.lower() == existing.lower() for existing in contract.hard_constraints): + return False, f"the agent already has that rule: {rule}" + + contract.hard_constraints.append(rule) + contract.amendments.append(f"rule added — {rule}: {why.strip()}") + problems = validate_contract(contract) + if problems: + contract.hard_constraints.pop() + contract.amendments.pop() + return False, "the amended contract would not be valid: " + "; ".join(problems) + + destination = Path(destination) + destination.mkdir(parents=True, exist_ok=True) + (destination / CONTRACT).write_text( + json.dumps(contract.model_dump(), indent=2, ensure_ascii=False), + encoding="utf-8", + ) + return True, ( + f"added. The agent now has {len(contract.hard_constraints)} rules, and this one is " + "graded from here on." + ) + + +def set_modality( + contract: AgentContract, destination: Path, *, modality: str, why: str +) -> tuple[bool, str]: + """Correct how a person actually reaches this agent. + + Worth its own amendment because modality is the one field that reroutes everything: it picks + the world, the simulator and the transport, so a wrong value does not degrade a run, it runs + a different test. And it is the field the source is least able to settle. An agent's code + reads the same whether it is answering a chat window or a phone call, so a reader with no + other evidence concludes whatever the repository looks like, which for a text benchmark is + text, even when the person asking said they had deployed it to a phone number. + + Where the agent is deployed is a fact about somebody's setup rather than about the source, so + when the two disagree the person is right and the source is describing a different runtime of + the same agent. Recorded like every other amendment, because it is still a change to what was + read. + """ + named = (modality or "").strip().lower() + if named not in MODALITIES: + return False, f"{named!r} is not a modality. It is one of: {', '.join(MODALITIES)}" + if not why.strip(): + return False, "say why: modality decides how every scenario is run" + if named == contract.modality: + return False, f"the contract already says {named}" + + was = contract.modality + contract.modality = named + contract.amendments.append(f"modality {was} -> {named}: {why.strip()}") + problems = validate_contract(contract) + if problems: + contract.modality = was + contract.amendments.pop() + return False, "the amended contract would not be valid: " + "; ".join(problems) + + destination = Path(destination) + destination.mkdir(parents=True, exist_ok=True) + (destination / CONTRACT).write_text( + json.dumps(contract.model_dump(), indent=2, ensure_ascii=False), + encoding="utf-8", + ) + reached = { + "voice": "a call placed to the agent where it is hosted, its own tools answered over a " + "webhook", + "chat": "a typed conversation, the agent reconstructed here from its contract", + "browser": "a browser the agent drives", + }[named] + return True, f"modality is now {named}. Every scenario will be run as {reached}." + + +def drop_rule( + contract: AgentContract, destination: Path, *, rule: str, why: str +) -> tuple[bool, str]: + """Take away a rule the agent does not really have. + + Stage one can misread a comment as a constraint, and a rule nobody has is worse than a + missing one: the agent under test is told to obey it and the judge fails it for not doing + something it was never supposed to do. + """ + if not why.strip(): + return False, "say why: removing a rule changes what is being graded" + match = next( + ( + existing + for existing in contract.hard_constraints + if existing.lower() == rule.strip().lower() + ), + None, + ) or next( + ( + existing + for existing in contract.hard_constraints + if rule.strip().lower() in existing.lower() + ), + None, + ) + if match is None: + return False, ( + "no rule like that. It has:\n - " + + "\n - ".join(contract.hard_constraints) + ) + contract.hard_constraints.remove(match) + contract.amendments.append(f"rule removed — {match}: {why.strip()}") + _persist(contract, destination) + return True, f"removed. {len(contract.hard_constraints)} rules left" + + +def fix_tool( + contract: AgentContract, + destination: Path, + *, + tool_name: str, + why: str, + args: list[str] | None = None, + arg_types: dict[str, str] | None = None, + description: str = "", + remove: bool = False, +) -> tuple[bool, str]: + """Correct a tool that was read wrong, or take away one the agent does not have. + + The most damaging thing stage one can get wrong. Every argument name flows into the world's + handlers, the probes and the scenarios, so a tool recorded with the wrong argument produces + a world that refuses everything and a suite that blames the agent for it. + """ + if not why.strip(): + return False, "say why: this changes what everything downstream is built from" + spec = next((tool for tool in contract.tools if tool.name == tool_name), None) + if spec is None: + return False, ( + f"{tool_name!r} is not a tool this agent has. It has: " + f"{', '.join(sorted(contract.tool_names()))}" + ) + + if remove: + contract.tools.remove(spec) + contract.amendments.append(f"tool removed — {tool_name}: {why.strip()}") + problems = validate_contract(contract) + if problems: + contract.tools.append(spec) + contract.amendments.pop() + return False, "cannot remove it: " + "; ".join(problems) + _persist(contract, destination) + return True, f"{tool_name} removed. {len(contract.tools)} tools left" + + changed = [] + if args is not None: + # Values recorded against an argument that no longer exists would silently be lost, so + # they are carried across by name and anything orphaned is said out loud. + orphaned = sorted(set(spec.arg_values) - set(args)) + spec.args = list(args) + spec.arg_types = {k: v for k, v in spec.arg_types.items() if k in spec.args} + spec.arg_values = {k: v for k, v in spec.arg_values.items() if k in spec.args} + changed.append(f"arguments are now {', '.join(args)}") + if orphaned: + changed.append(f"dropped values recorded for {', '.join(orphaned)}") + if arg_types: + unknown = sorted(set(arg_types) - set(spec.args)) + if unknown: + return False, f"{tool_name} takes no argument called {', '.join(unknown)}" + spec.arg_types.update(arg_types) + changed.append("types updated") + if description: + spec.description = description + changed.append("description updated") + if not changed: + return False, "nothing to change: give args, arg_types, description, or remove" + + contract.amendments.append(f"tool corrected — {tool_name}: {why.strip()}") + problems = validate_contract(contract) + if problems: + return False, "the amended contract would not be valid: " + "; ".join(problems) + _persist(contract, destination) + return True, f"{tool_name}: {', '.join(changed)}" + + +def _persist(contract: AgentContract, destination: Path) -> None: + destination = Path(destination) + destination.mkdir(parents=True, exist_ok=True) + (destination / CONTRACT).write_text( + json.dumps(contract.model_dump(), indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + +def not_offered(contract: AgentContract, candidates: dict[str, set[str]]) -> list[str]: + """For each argument, the candidate values the contract does not let the agent send. + + ``candidates`` maps an argument name to identifiers found in the world that plausibly belong + to it. Kept as an argument rather than inferred here, because which column feeds which + argument is knowledge about one agent, not something a schema states. + """ + missing: list[str] = [] + for tool in contract.tools: + for argument, values in (tool.arg_values or {}).items(): + if argument not in candidates or not isinstance(values, (list, tuple)): + continue + permitted = {str(value) for value in values} + absent = sorted(candidates[argument] - permitted) + if absent: + missing.append(f"{tool.name}.{argument}: {', '.join(absent)}") + return missing + + +def unreachable( + contract: AgentContract, + destination: Path, + *, + tool_name: str, + why: str, +) -> tuple[bool, str]: + """Record that a tool's own implementation cannot be run here, and let one be written instead. + + The harness refuses to write a replacement for a tool the agent already implements, because a + stand-in that looks right is worse than a tool we admit we could not run. But some + implementations genuinely cannot be reached: they are built by a framework that needs a live + client, or they live in a package this environment does not have. Without a way to say so, the + build has no legitimate exit at all, and the only ways forward are to give up or to lie. + + So this is the exit, and it costs something: the reason is recorded on the contract, next to + the tool, permanently. Anyone reading it afterwards can tell which tools ran the agent's own + code and which were stand-ins, which is the distinction the refusal exists to protect. + """ + spec = next((tool for tool in contract.tools if tool.name == tool_name), None) + if spec is None: + return False, ( + f"{tool_name!r} is not a tool this agent has. It has: " + f"{', '.join(sorted(contract.tool_names()))}" + ) + if not why.strip(): + return False, ( + "say why it cannot be reached. An unexplained stand-in is indistinguishable from not " + "having tried, and this is the one record that it was a stand-in at all." + ) + entry = contract.entry_for(tool_name) + if entry is None or entry.mode == "generate": + return False, ( + f"{tool_name} has no implementation recorded, so nothing is blocking a handler for " + "it. Write one with define_handler." + ) + + was = entry.mode + entry.mode = "generate" + entry.notes = (f"{entry.notes} " if entry.notes else "") + f"unreachable here: {why.strip()}" + contract.amendments.append( + f"{tool_name} was recorded as {was} but could not be reached, so the world implements it: " + f"{why.strip()}" + ) + + destination = Path(destination) + destination.mkdir(parents=True, exist_ok=True) + (destination / CONTRACT).write_text( + json.dumps(contract.model_dump(), indent=2, ensure_ascii=False), + encoding="utf-8", + ) + return True, ( + f"Recorded: {tool_name} could not be run from the agent's own code here. define_handler " + "will now accept one for it, and the contract carries the reason so nobody later reads " + "this as the agent's own tool having been tested." + ) diff --git a/harness/src/agent_harness/build.py b/harness/src/agent_harness/build.py new file mode 100644 index 0000000..dbb2782 --- /dev/null +++ b/harness/src/agent_harness/build.py @@ -0,0 +1,100 @@ +"""Stage two: build the world the agent's tools run against. + +Reads the contract stage one produced and builds a database behind the agent's action space, +then freezes it. The frozen snapshot is the base state every scenario restores from; a scenario +adds only the rows it additionally needs. + +The stage stays open, because a world is usually right on the second look. Correcting a handler +is the next thing said, and the tool is re-run on the spot. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Callable + +from claude_agent_sdk import ClaudeAgentOptions + +from .config import ( + artifact_dir, + UNWANTED, + gate_hooks, + chosen_model, + load_skill, + permission_gate, + provider_env, +) +from .contract import AgentContract +from .world.snapshot import saved as world_saved +from .session import Stage +from .tools import qualified +from .world.tools import TOOL_NAMES, WORLD_SERVER, world_tools + +SKILL = "build-environment" + + +def open_stage( + contract: AgentContract, + *, + out: Path | None = None, + ask: Callable[..., Any] | None = None, + source_root: str = "", + max_turns: int = 60, +) -> tuple[Stage, Path]: + """A live build-the-world stage, and where it will write.""" + destination = out or artifact_dir(contract.agent) + server, _world = world_tools(contract, destination, source_root=source_root) + allowed = [ + "AskUserQuestion", + *(qualified(WORLD_SERVER, name) for name in TOOL_NAMES), + ] + options = ClaudeAgentOptions( + system_prompt=( + f"{load_skill(SKILL)}\n\n## This agent\n\n{contract.brief(with_data=True)}" + ), + # No file tools and no shell. Everything this stage can do goes through a tool that + # executes it and reports back, which is what makes the guardrails meaningful. + allowed_tools=allowed, + mcp_servers={WORLD_SERVER: server}, + # Not acceptEdits: that auto-approves Edit and Write before the permission callback is + # consulted, so a stage can rewrite an artifact by hand and skip the tool whose + # whole job is to validate that change. + permission_mode="default", + cwd=str(destination.parent if destination.parent.exists() else Path.cwd()), + setting_sources=[], + max_turns=max_turns, + model=chosen_model(), + env=provider_env(), + ) + options.disallowed_tools = list(UNWANTED) + options.hooks = gate_hooks(allowed) + options.can_use_tool = permission_gate(ask, allowed) + return Stage(options, name=SKILL), destination + + +def opening(contract: AgentContract) -> str: + return ( + f"Build the world for {contract.agent!r}.\n\n" + "Design the schema, seed it from the contract's real data, and write one handler per " + "tool. Verify the refusals yourself with run_tool: a call naming something that does " + "not exist must be refused, not succeed. Declare at least one sequence where state has " + "to carry across calls, then check_world and save_world." + ) + + +async def build( + contract: AgentContract, + *, + out: Path | None = None, + follow_ups: list[str] | None = None, + on_event: Callable[..., Any] | None = None, + ask: Callable[..., Any] | None = None, + max_turns: int = 60, +) -> Path | None: + """Run the stage start to finish. Returns where the world was written, or None.""" + stage, destination = open_stage(contract, out=out, ask=ask, max_turns=max_turns) + async with stage: + await stage.say(opening(contract), on_event=on_event) + for follow_up in follow_ups or []: + await stage.say(follow_up, on_event=on_event) + return destination if world_saved(destination) else None diff --git a/harness/src/agent_harness/catalogue.py b/harness/src/agent_harness/catalogue.py new file mode 100644 index 0000000..a85564d --- /dev/null +++ b/harness/src/agent_harness/catalogue.py @@ -0,0 +1,129 @@ +"""The sub-goals this agent can be checked on, shared by every scenario that needs one. + +Defined once for the agent rather than restated per scenario, which is what makes results roll +up: the same sub-goal failing in seven of twelve scenarios is one sentence rather than seven. + +``check`` is Python written by the harness. It is given what the run left behind and returns +nothing if the sub-goal held, or a sentence saying what was wrong. Code rather than a mini +language because an environment can be a database, a filesystem or a page, and a language +invented here would fit only the first. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from pydantic import BaseModel, Field + +CATALOGUE = "sub_goals.json" + + +class SubGoal(BaseModel): + """One named thing the agent can be checked on, shared across every scenario that needs it. + + ``check`` is Python, written by the harness. It is given what the run left behind and returns + nothing if the sub-goal held, or a sentence saying what was wrong. Code rather than a mini + language because an environment can be a database, a filesystem or a page, and a language + invented here would fit only the first. + + ``judged`` marks the ones nothing observable can settle — whether a refusal was explained, + whether a price was invented. Those go to a model, and are the exception. + """ + + name: str + what: str = "" + check: str = "" + judged: str = "" + + def deterministic(self) -> bool: + return bool(self.check.strip()) + + +class SuiteEval(BaseModel): + """One built-in Future AGI eval applied to every compatible scenario.""" + + name: str + required_inputs: list[str] = Field(default_factory=lambda: ["conversation"]) + minimum_score: float | None = None + + +def default_suite_evals() -> list[SuiteEval]: + """The two verified built-in evals initially run for every voice scenario.""" + return [ + SuiteEval( + name="customer_agent_task_completion", + required_inputs=["agent_prompt", "conversation"], + ), + SuiteEval( + name="customer_agent_conversation_quality", + minimum_score=4, + ), + ] + + +class Catalogue(BaseModel): + """Every sub-goal this agent has, defined once.""" + + sub_goals: list[SubGoal] = Field(default_factory=list) + # Deliberately separate from sub-goals: these assess every scenario, while a sub-goal only + # applies where a scenario names it. + suite_evals: list[SuiteEval] = Field(default_factory=default_suite_evals) + + def named(self, name: str) -> SubGoal | None: + return next((one for one in self.sub_goals if one.name == name), None) + + def names(self) -> set[str]: + return {one.name for one in self.sub_goals} + + def suite_eval(self, name: str) -> SuiteEval | None: + return next((one for one in self.suite_evals if one.name == name), None) + + +def validate_suite_eval(suite_eval: SuiteEval) -> list[str]: + if not suite_eval.name.strip(): + return ["no name"] + if not suite_eval.required_inputs: + return [f"{suite_eval.name}: no required inputs"] + return [] + + +def validate_sub_goal(sub_goal: SubGoal) -> list[str]: + """Problems that make a sub-goal unusable. + + A sub-goal that settles nothing is the expensive kind of wrong: every scenario referencing it + reports a result nobody should believe. + """ + problems: list[str] = [] + if not sub_goal.name.strip(): + problems.append("no name") + if not sub_goal.what.strip(): + problems.append(f"{sub_goal.name}: no description of what it means") + if not sub_goal.check.strip() and not sub_goal.judged.strip(): + problems.append( + f"{sub_goal.name}: settles nothing. Give a check in code, or say what a judge has " + "to decide and why nothing observable can settle it" + ) + if sub_goal.check.strip() and "def check(" not in sub_goal.check: + problems.append( + f"{sub_goal.name}: a check must define check(world, calls) and return a problem as " + "a string, or None when the sub-goal held" + ) + return problems + + +def save_catalogue(catalogue: Catalogue, destination: Path) -> Path: + destination = Path(destination) + destination.mkdir(parents=True, exist_ok=True) + path = destination / CATALOGUE + path.write_text( + json.dumps(catalogue.model_dump(), indent=2, ensure_ascii=False), encoding="utf-8" + ) + return path + + +def load_catalogue(destination: Path) -> Catalogue: + path = Path(destination) / CATALOGUE + if not path.exists(): + return Catalogue() + return Catalogue.model_validate(json.loads(path.read_text(encoding="utf-8"))) diff --git a/harness/src/agent_harness/chat.py b/harness/src/agent_harness/chat.py new file mode 100644 index 0000000..64c3cb8 --- /dev/null +++ b/harness/src/agent_harness/chat.py @@ -0,0 +1,408 @@ +"""One conversation, from pointing at an agent to a world you can test against. + +You say what you want, it does it, you say the next thing. Stages are not commands you invoke; +they are what the harness moves through while you keep talking. When one produces its artifact +the next opens on the same agent, and anything already built stays correctable by saying so. + +Underneath, each stage is still its own session with its own instructions and its own tools, so +context stays small and a stage can be re-entered later without redoing the ones before it. That +is an implementation detail, not something to make somebody manage. +""" + +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from . import build as build_stage +from . import reception as reception_stage +from . import scenarios as scenario_stage +from . import understand as understand_stage +from .config import artifact_dir +from .contract import AgentContract +from .run import stage as run_stage +from .session import Stage +from .sources import AgentSource, resolve +from .world.snapshot import saved as world_saved + +RECEPTION = "reception" +UNDERSTAND = "understand" +BUILD = "build" +SCENARIOS = "scenarios" +RUN = "run" +DONE = "done" + +_NEXT = { + RECEPTION: UNDERSTAND, + UNDERSTAND: BUILD, + BUILD: SCENARIOS, + SCENARIOS: RUN, + RUN: DONE, +} + + +@dataclass +class Conversation: + """The whole thing, held open.""" + + # Both unknown until somebody says which agent this is about, which is itself a stage. + source: AgentSource | None = None + out: Path | None = None + ask: Callable[..., Any] | None = None + wanted: int = 10 + # Where to look for an agent. Almost never inside this repo: the harness lives in one place + # and the agent being tested lives in another, so looking only at our own root means the + # first thing anybody types cannot be found. + workspace: Path | None = None + stage_name: str = "" + stage: Stage | None = None + # Set by the flow tool when the open stage hands a request to the stage that owns it. + _handoff: dict = field(default_factory=dict) + spent_usd: float = 0.0 + history: list[str] = field(default_factory=list) + _found: dict[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + # Read off the artifacts rather than defaulting to the first stage. An agent whose world + # is already built is at the scenarios, and saying otherwise before anything has been + # opened makes every question about where this conversation is answer wrongly. + self.stage_name = self.stage_name or self._resume_at() + + # -- what exists so far ---------------------------------------------------------- + + @property + def contract(self) -> AgentContract | None: + return understand_stage.load(self.out) if self.out else None + + @property + def world_built(self) -> bool: + # The manifest, not a database file. Every saved world writes one; only some of them have + # a SQLite file beside it, and an agent whose state lives in services and files has none. + # Keyed on the database, such a world stays "not built" forever and the conversation can + # never leave this stage, however well the build actually went. + return world_saved(self.out) + + @property + def scenarios_written(self) -> bool: + return bool(self.out) and bool(scenario_stage.load(self.out)) + + @property + def anything_run(self) -> bool: + return bool(self.out) and bool(run_stage.load(self.out)) + + def _artifact_for(self, stage_name: str) -> bool: + return { + # A contract already on disk settles which agent this is just as well as being told, + # so coming back to an agent does not mean pointing at its repository again. + # + # ``_found`` is checked too, because within the turn that points at an agent the + # source is not on the conversation yet — it is read off afterwards. Without it, the + # stage that has just succeeded is told it has produced nothing. + RECEPTION: self.source is not None + or self.contract is not None + or self._found.get("source") is not None, + UNDERSTAND: self.contract is not None, + BUILD: self.world_built, + SCENARIOS: self.scenarios_written, + RUN: self.anything_run, + DONE: True, + }[stage_name] + + # -- moving between stages ------------------------------------------------------- + + async def _close(self) -> None: + if self.stage is not None: + self.spent_usd += self.stage.spent_usd + await self.stage.__aexit__(None, None, None) + self.stage = None + + async def _open(self, stage_name: str) -> str: + """Open a stage and return the message that starts it.""" + await self._close() + self.stage_name = stage_name + if stage_name == RECEPTION: + self.stage, self._found = reception_stage.open_stage( + cwd=self.workspace, + ask=self.ask, + # The UI allocates a session before the agent has a name. Put a GitHub clone in + # that existing session rather than creating a second artifact directory. + source_dir=(self.out / "source") if self.out else None, + ) + self._grant_flow() + await self.stage.__aenter__() + return reception_stage.opening() + + # Deliberately not "is there a source": a contract on disk settles which agent this is, + # and every stage after the first works from the contract rather than from the source. + # Only re-reading the agent needs to know where it lives. + if self.source is None and self.contract is None: + raise RuntimeError("nobody has said which agent this is about yet") + if stage_name == UNDERSTAND and self.source is None: + # This guard has to come before the stage opens: with a contract on disk but no + # source, reopening understand would otherwise die on source.briefing() instead of + # saying what is actually missing. + raise RuntimeError("cannot re-read the agent without knowing where it lives") + if stage_name == UNDERSTAND: + self.stage, _ = understand_stage.open_stage( + self.source, out=self.out, ask=self.ask + ) + opening = understand_stage.opening(self.source) + self._grant_flow() + await self.stage.__aenter__() + return opening + + contract = self.contract + if contract is None: + raise RuntimeError("cannot go further before there is a contract") + if stage_name == BUILD: + self.stage, _ = build_stage.open_stage( + contract, + out=self.out, + ask=self.ask, + # Where the agent's own code lives, so its tools can be bound to rather + # than rewritten. Empty for an agent given as a specification. + source_root=str(getattr(self.source, "root", "") or ""), + ) + opening = build_stage.opening(contract) + elif stage_name == RUN: + if not self.scenarios_written: + raise RuntimeError("cannot run anything before there are scenarios") + self.stage, _ = run_stage.open_stage(contract, out=self.out, ask=self.ask) + opening = run_stage.opening(contract, self.out) + else: + if not self.world_built: + raise RuntimeError("cannot write scenarios before there is a world") + written = len(scenario_stage.load(self.out)) + wanted = written or self.wanted + self.stage, _ = scenario_stage.open_stage( + contract, out=self.out, wanted=wanted, ask=self.ask + ) + opening = scenario_stage.opening(contract, wanted, written) + self._grant_flow() + await self.stage.__aenter__() + return opening + + def next_stage(self) -> str | None: + """The stage that follows the current one, once this one has produced its artifact.""" + if not self._artifact_for(self.stage_name): + return None + following = _NEXT.get(self.stage_name) + return None if following in (None, DONE) else following + + def _flow_server(self): + """One tool every stage gets: handing a request to the stage that owns it. + + "Create the world", said while the understand stage is open, used to land in a session + with no build tools, which could only apologise. The stage is the one that knows the + request is not its job, so the handoff is a tool it calls; whether moving on is allowed + is still decided by code, from whether this stage's artifact exists. + """ + from claude_agent_sdk import create_sdk_mcp_server, tool + + from .tools import schema + + wanted = self._handoff + + @tool( + "hand_to_next_stage", + "The person asked for something that belongs to the NEXT stage of this harness — " + "building the environment when the contract is done, writing scenarios when the " + "environment is built, running them when they are written. Call this with their " + "request, word for word; the conversation moves forward and their request is " + "handled there. Never call it to escape work that is this stage's own.", + schema({"request": str}, []), + ) + async def hand_to_next_stage(args: dict[str, Any]) -> dict[str, Any]: + if not self._artifact_for(self.stage_name): + return { + "content": [{ + "type": "text", + "text": "This stage has not produced its artifact yet, so there is " + "nothing to move on from. Finish this stage's work first.", + }], + "is_error": True, + } + if self.next_stage() is None: + return { + "content": [{"type": "text", "text": "there is no stage after this one"}], + "is_error": True, + } + wanted["request"] = str(args.get("request") or "").strip() or "continue" + return { + "content": [{ + "type": "text", + "text": "Handed over. Say one short line that you are moving on, and stop.", + }] + } + + return create_sdk_mcp_server(name="flow", version="0.1.0", tools=[hand_to_next_stage]) + + # -- talking --------------------------------------------------------------------- + + async def start(self, on_event: Callable[..., Any] | None = None) -> None: + """Open the stage this agent is up to, and set it going.""" + opening = await self._open(self._resume_at()) + await self.stage.say(opening, on_event=on_event) # type: ignore[union-attr] + + async def open_quietly(self) -> None: + """Open the stage without telling it to start. + + A stage's opening message is an instruction to do the stage's work. Sending it because + somebody said hello means a greeting kicks off a build, so it is only sent when the work + is actually what was asked for. + """ + await self._open(self._resume_at()) + + def _resume_at(self) -> str: + """Pick up where the artifacts say this agent got to.""" + if self.source is None and self.contract is None: + return RECEPTION + if self.contract is None: + return UNDERSTAND + if not self.world_built: + return BUILD + if not self.scenarios_written: + return SCENARIOS + return RUN + + def _grant_flow(self) -> None: + if self.stage is not None: + self.stage.grant("flow", self._flow_server(), ["hand_to_next_stage"], ask=self.ask) + + async def say( + self, message: str, on_event: Callable[..., Any] | None = None + ) -> None: + """Send a message to whichever stage is open.""" + self.history.append(message) + if self.stage is None: + await self.open_quietly() + await self.stage.say(message, on_event=on_event) # type: ignore[union-attr] + # Before anything acts on this turn, take up what it established. A handoff in the same + # turn opens the next stage, and every stage is built from ``self.source``; read it off + # afterwards instead and that hop dies on an agent nobody has named, taking the turn with + # it and leaving the conversation in reception with no way forward. + established = self._take_up() + moved = False + # A handoff moves the request, not just the conversation: the next stage opens and is + # given the person's own words. Bounded, because each hop is a model turn. + for _hop in range(3): + request = self._handoff.pop("request", None) + if not request: + break + following = self.next_stage() + if following is None: + break + await self._open(following) + moved = True + await self.stage.say(request, on_event=on_event) # type: ignore[union-attr] + if established and not moved: + # Nothing is left to decide once the agent is known, so it goes on rather than making + # somebody confirm what they already said. Unless a handoff already moved us, which + # would make this a second hop over the same request. + await self.advance(on_event=on_event) + + def _take_up(self) -> bool: + """Take up whatever the turn just established. True if this turn named the agent. + + Reception is the only stage whose result is not a file, so it is the only one the + conversation has to read back. + """ + settled = self._found.pop("source", None) + if settled is None: + return False + self.source = settled + self.out = self.out or artifact_dir(settled.name) + return True + + def reachable(self) -> dict[str, str]: + """Every stage, and why it can or cannot be opened right now. + + Stages are not a wizard. Coming back to correct a contract after the world is built is + the ordinary case, not an exception, so any stage whose input exists can be opened at + any time. What cannot be skipped is the input itself: there is nothing to build a world + from without a contract, and nothing to write scenarios against without a world. + """ + contract = self.contract is not None + # Every stage after the first works from the contract, so that is the first thing each + # of them needs; its own input is the second. + needs_contract = "needs a contract first" + why = { + RECEPTION: "", + UNDERSTAND: "" + if self.source is not None + else "cannot re-read the agent without knowing where its source lives", + BUILD: "" if contract else needs_contract, + SCENARIOS: "" + if contract and self.world_built + else (needs_contract if not contract else "needs a built environment first"), + RUN: "" + if contract and self.scenarios_written + else (needs_contract if not contract else "needs scenarios first"), + } + return why + + async def go_to( + self, stage_name: str, on_event: Callable[..., Any] | None = None + ) -> str: + """Open one stage by name, whether or not it is the next one. + + The stage is opened but not set going: its opening message is an instruction to do that + stage's work, and somebody choosing to look at a stage has not thereby asked for it to + start spending. + """ + if stage_name not in _NEXT and stage_name != DONE: + raise RuntimeError(f"no stage called {stage_name!r}") + blocked = self.reachable().get(stage_name, "") + if blocked: + raise RuntimeError(f"cannot open the {stage_name} stage: {blocked}") + return await self._open(stage_name) + + async def advance(self, on_event: Callable[..., Any] | None = None) -> str | None: + """Move to the next stage and start it. Returns the stage entered, or None.""" + following = self.next_stage() + if following is None: + return None + opening = await self._open(following) + await self.stage.say(opening, on_event=on_event) # type: ignore[union-attr] + return following + + async def close(self) -> None: + await self._close() + + +def open_conversation( + *, + name: str = "", + path: str = "", + kind: str = "repo", + out: Path | None = None, + ask: Callable[..., Any] | None = None, + wanted: int = 10, + workspace: Path | None = None, +) -> Conversation: + """Open the harness. With nothing, it starts by asking which agent you mean. + + Naming the agent up front is a shortcut for coming back to one already in progress, not the + way in. Everything it needs can be said. + """ + source = resolve(kind, name=name, root=path) if name and path else None + return Conversation( + source=source, + out=out or (artifact_dir(name) if name else None), + ask=ask, + wanted=wanted, + workspace=workspace, + ) + + +async def _demo() -> None: # pragma: no cover - convenience for manual runs + conversation = open_conversation(name="demo", path=".") + await conversation.start() + await conversation.close() + + +if __name__ == "__main__": # pragma: no cover + asyncio.run(_demo()) diff --git a/harness/src/agent_harness/checks.py b/harness/src/agent_harness/checks.py new file mode 100644 index 0000000..79040c6 --- /dev/null +++ b/harness/src/agent_harness/checks.py @@ -0,0 +1,111 @@ +"""Running a check the harness wrote, and deciding what its answer means. + +A check is Python because an environment can be a database, a filesystem or a page, and any +little assertion language invented here would fit only the first. It is given the two things a +run leaves behind and returns a sentence when something is wrong: + + def check(world, calls): + rows = world.state()["orders"] + if len(rows) != 1: + return f"{len(rows)} orders, expected 1" + if not any(c.name == "order_combo_meal" for c in calls): + return "the combo was never ordered" + return None + +``world`` is the environment afterwards. ``calls`` is every tool call that was made, each with +its arguments and whether it succeeded — so a check can insist not only that a call happened but +that it happened with the right arguments, which is the difference between booking 11 PM and +booking 10 PM. + +A check that raises is a broken check, not a failed one, and is reported that way. Confusing the +two would let a typo read as a finding about the agent. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Sequence + +from .world.runtime import Call, GeneratedWorld + + +@dataclass +class Outcome: + """What one check said.""" + + name: str + held: bool + said: str = "" + broken: bool = False + + def line(self) -> str: + mark = "!" if self.broken else ("x" if self.held else " ") + return f" [{mark}] {self.name}" + (f" — {self.said}" if self.said else "") + + +def run_check( + source: str, world: GeneratedWorld, calls: Sequence[Call], *, name: str = "check" +) -> Outcome: + """Execute one check against what the run left behind.""" + namespace: dict[str, Any] = {} + try: + exec(compile(source, f"", "exec"), namespace) + except Exception as failed: + return Outcome(name, False, f"the check would not compile: {failed}", broken=True) + + checker = namespace.get("check") + if not callable(checker): + return Outcome(name, False, "the check defines no check(world, calls)", broken=True) + + try: + said = checker(world, list(calls)) + except Exception as failed: + # The check is at fault, not the agent. A KeyError in an assertion is our bug, and + # scoring it against the agent is how a harness invents findings. + return Outcome( + name, False, f"the check raised {type(failed).__name__}: {failed}", broken=True + ) + + if said is None or said is True: + return Outcome(name, True) + return Outcome(name, False, str(said) if said is not True else "") + + +def all_held(outcomes: Sequence[Outcome]) -> bool: + return all(one.held for one in outcomes) and not any(one.broken for one in outcomes) + + +def broken(outcomes: Sequence[Outcome]) -> list[Outcome]: + return [one for one in outcomes if one.broken] + +def run_world_check(source: str, world: GeneratedWorld, *, name: str = "check") -> Outcome: + """Execute one check about the world itself, rather than about a run. + + A world check asks whether the environment is usable at all, so it is written ``check(world)`` + and there are no calls to give it. Both arities are accepted, because the difference is not + worth a rejection: a check written ``check(world, calls)`` out of habit is answering the same + question, and gets an empty list. + """ + import inspect + + namespace: dict[str, Any] = {} + try: + exec(compile(source, f"", "exec"), namespace) + except Exception as failed: + return Outcome(name, False, f"the check would not compile: {failed}", broken=True) + + checker = namespace.get("check") + if not callable(checker): + return Outcome(name, False, "the check defines no check(world)", broken=True) + + try: + wants = len(inspect.signature(checker).parameters) + except (TypeError, ValueError): + wants = 1 + try: + said = checker(world) if wants < 2 else checker(world, []) + except Exception as failed: + return Outcome( + name, False, f"the check raised {type(failed).__name__}: {failed}", broken=True + ) + return Outcome(name, said is None, "" if said is None else str(said)) diff --git a/harness/src/agent_harness/cli.py b/harness/src/agent_harness/cli.py new file mode 100644 index 0000000..0bf37b8 --- /dev/null +++ b/harness/src/agent_harness/cli.py @@ -0,0 +1,457 @@ +"""Run a stage from a terminal. + +This is one renderer over the stage loop, not the product. It prints events as lines and reads +follow-ups from stdin; a browser front end subscribes to the same events and draws them as a +transcript beside the artifact. Keeping the terminal a renderer rather than the interface is what +makes the second one cheap. +""" + +from __future__ import annotations + +import argparse +import asyncio +import sys +from pathlib import Path +from typing import Any + +from .build import open_stage as build_stage +from .build import opening as build_opening +from .chat import open_conversation +from .config import ( + DEFAULT_MODEL, + artifact_dir, + chosen_model, + credentials_hint, + permission_gate, +) +from .scenarios import load as load_written +from .scenarios import open_stage as scenario_stage +from .scenarios import opening as scenario_opening +from .session import TEXT, Event +from .world.snapshot import saved as world_saved +from .run.targets import supported as target_kinds +from .sources import resolve, supported +from .understand import load, open_stage, opening + + +def _render(event: Event) -> None: + line = event.line() + if event.kind == TEXT: + print(line, end="", flush=True) + else: + print(f"\n{line}", flush=True) + + +async def _prompt(question: str) -> str: + return (await asyncio.to_thread(input, question)).strip() + + +async def _ask_operator(_tool_name: str, payload: dict[str, Any], _context: Any) -> Any: + """Render the model's clarifying questions and return the operator's answers.""" + from claude_agent_sdk.types import PermissionResultAllow + + answers: dict[str, Any] = {} + for question in payload.get("questions", []): + print(f"\n\n {question.get('header', '?')}: {question.get('question', '')}") + options = question.get("options", []) or [] + for index, option in enumerate(options, start=1): + print( + f" {index}. {option.get('label')} - {option.get('description', '')}" + ) + raw = await _prompt(" > ") + chosen = raw + if raw.isdigit() and 1 <= int(raw) <= len(options): + chosen = options[int(raw) - 1].get("label", raw) + answers[question.get("question", "")] = chosen + print() + return PermissionResultAllow( + updated_input={"questions": payload.get("questions", []), "answers": answers} + ) + + +async def _understand(args: argparse.Namespace) -> int: + source = resolve(args.kind, name=args.name, root=args.path) + stage, destination = open_stage( + source, + out=Path(args.out) if args.out else None, + # Unattended, there is nobody to answer, so the model records what it could not + # resolve in open_questions rather than blocking on a prompt nobody will see. + ask=permission_gate(_ask_operator) if args.interactive else None, + ) + + print(f"agent: {source.name} ({source.kind})") + print(f"model: {chosen_model()}") + print(f"out: {destination}\n") + + await _converse( + stage, + opening(source), + interactive=args.interactive, + until=lambda: load(destination) is not None, + nudge=( + "Nothing was saved: you finished without calling submit_contract. Call it now " + "with the contract you worked out." + ), + ) + + contract = load(destination) + if contract is None: + print("\nNo contract was submitted.", file=sys.stderr) + return 1 + print( + f"\ncontract: {len(contract.tools)} tools, " + f"{len(contract.hard_constraints)} rules, " + f"{len(contract.real_use_cases)} use cases, " + f"{len(contract.open_questions)} open questions" + ) + print(f"spent: ${stage.spent_usd:.4f}") + return 0 + + +async def _converse( + stage, + opening_message: str, + *, + interactive: bool, + until=None, + nudge: str = "", +) -> None: + """Say the opening, then keep the stage open for corrections. + + The same shape for every stage. A world is usually right on the second look, and the point + of holding the session open is that correcting it is the next thing said rather than a + rebuild from nothing. + + ``until``/``nudge`` guard the unattended case. The commonest way an unattended stage fails + is finishing all the work and never calling the tool that saves it — the whole contract + written out as prose, submitted to nobody. One mechanical reminder costs a turn; rerunning + the stage costs everything it just did. + """ + async with stage: + await stage.say(opening_message, on_event=_render) + if not interactive and until is not None and nudge and not until(): + await stage.say(nudge, on_event=_render) + while interactive: + try: + said = await _prompt("\nyou ") + except (EOFError, KeyboardInterrupt): + break + if not said or said in {"q", "quit", "exit"}: + break + await stage.say(said, on_event=_render) + + +async def _build(args: argparse.Namespace) -> int: + destination = Path(args.out) if args.out else artifact_dir(args.name) + contract = load(destination) + if contract is None: + print(f"No contract at {destination}. Run `understand` first.", file=sys.stderr) + return 1 + + print(f"agent: {contract.agent} ({len(contract.tools)} tools)") + print(f"model: {chosen_model()}") + print(f"out: {destination}\n") + + stage, _ = build_stage( + contract, + out=destination, + ask=permission_gate(_ask_operator) if args.interactive else None, + ) + await _converse( + stage, + build_opening(contract), + interactive=args.interactive, + until=lambda: world_saved(destination), + nudge=( + "Nothing was saved: you finished without calling save_world. Call check_world, " + "fix what it names, then save_world." + ), + ) + + if not world_saved(destination): + print("\nNo world was saved.", file=sys.stderr) + return 1 + print(f"\nworld: {destination}") + print(f"spent: ${stage.spent_usd:.4f}") + return 0 + + +async def _scenarios(args: argparse.Namespace) -> int: + destination = Path(args.out) if args.out else artifact_dir(args.name) + contract = load(destination) + if contract is None: + print(f"No contract at {destination}. Run `understand` first.", file=sys.stderr) + return 1 + if not world_saved(destination): + print(f"No world at {destination}. Run `build` first.", file=sys.stderr) + return 1 + + # With a suite already written, the target is what is there. Somebody who comes back to + # change one scenario is not asking for a different number of them. + existing = len(load_written(destination)) + wanted = args.count or existing or 10 + + print( + f"agent: {contract.agent} " + + (f"({existing} scenarios, loaded)" if existing else f"(writing {wanted})") + ) + print(f"model: {chosen_model()}") + print(f"out: {destination}\n") + + stage, _ = scenario_stage( + contract, + out=destination, + wanted=wanted, + ask=permission_gate(_ask_operator) if args.interactive else None, + ) + await _converse( + stage, + scenario_opening(contract, wanted, existing), + interactive=args.interactive, + until=lambda: bool(load_written(destination)), + nudge=( + "Nothing was saved: you finished without calling save_scenarios. Submit anything " + "still unsubmitted, then call save_scenarios." + ), + ) + + written = load_written(destination) + if not written: + print("\nNo scenarios were saved.", file=sys.stderr) + return 1 + print(f"\nscenarios: {len(written)} in {destination / 'scenarios.json'}") + print(f"spent: ${stage.spent_usd:.4f}") + return 0 + + +async def _live(args: argparse.Namespace) -> int: + """The run stage as a conversation: it decides what to run and reads what came back.""" + from .run.stage import load as load_results + from .run.stage import open_stage as run_stage + from .run.stage import opening as run_opening + + destination = Path(args.out) if args.out else artifact_dir(args.name) + contract = load(destination) + written = load_written(destination) + if contract is None or not written: + print( + f"Need a contract and scenarios at {destination}. Run `understand`, `build` and " + "`scenarios` first.", + file=sys.stderr, + ) + return 1 + + print(f"agent: {contract.agent} ({len(written)} scenarios)") + print(f"model: {chosen_model()}") + print(f"out: {destination}\n") + + stage, _ = run_stage( + contract, + out=destination, + ask=permission_gate(_ask_operator) if args.interactive else None, + ) + await _converse( + stage, run_opening(contract, destination), interactive=args.interactive + ) + + results = load_results(destination) + passed = sum(1 for record in results if record["passed"]) + print(f"\nruns: {passed} of {len(results)} passed, in {destination / 'runs.json'}") + print(f"spent: ${stage.spent_usd:.4f}") + return 0 + + +async def _run(args: argparse.Namespace) -> int: + from .run import run_suite + from .run.grade import summarise + + destination = Path(args.out) if args.out else artifact_dir(args.name) + contract = load(destination) + written = load_written(destination) + if contract is None or not written: + print( + f"Need a contract and scenarios at {destination}. Run `understand`, `build` " + "and `scenarios` first.", + file=sys.stderr, + ) + return 1 + + chosen = [s for s in written if s.name in args.only] if args.only else written + if not chosen: + print(f"No scenario matching {args.only}.", file=sys.stderr) + return 1 + + print(f"agent: {contract.agent} ({len(chosen)} scenarios, target {args.target})") + print(f"model: {chosen_model()}") + print(f"out: {destination}\n") + + def overheard(exchange: Any) -> None: + if args.quiet: + return + print(f" {exchange.speaker:8} {exchange.text}", flush=True) + + def show(result: Any) -> None: + # Just the verdict as it lands. The detail is in the summary at the end, and printing + # it in both places means every failure is read twice. + print(result.line(), flush=True) + + results = await run_suite( + chosen, + contract, + destination, + target=args.target, + model=args.model, + on_result=show, + on_exchange=overheard, + ) + print("\n" + summarise(results)) + print(f"\nspent: ${sum(result.spent_usd for result in results):.4f}") + return 0 if all(result.passed for result in results) else 2 + + +async def _chat(args: argparse.Namespace) -> int: + """One conversation for the whole thing: point at an agent and keep talking.""" + conversation = open_conversation( + name=args.name or "", + path=args.path or "", + kind=args.kind, + out=Path(args.out) if args.out else None, + ask=permission_gate(_ask_operator), + ) + print(f"model: {chosen_model()}") + print(credentials_hint()) + print("\nSay what you want. Enter on its own moves to the next stage; 'q' ends.\n") + + await conversation.start(on_event=_render) + while True: + try: + said = await _prompt(f"\nyou ({conversation.stage_name}) ") + except (EOFError, KeyboardInterrupt): + break + if said in {"q", "quit", "exit"}: + break + if not said: + entered = await conversation.advance(on_event=_render) + if entered is None: + print( + "\n [nothing to move on to yet; this stage has not produced its artifact]" + ) + continue + await conversation.say(said, on_event=_render) + await conversation.close() + print(f"\nspent: ${conversation.spent_usd:.4f}") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="agent-harness", description=__doc__) + # Talking to it is the way in, so that is what happens when you just start it. + sub = parser.add_subparsers(dest="stage", required=False) + + understand = sub.add_parser( + "understand", help="read an agent and produce its contract" + ) + understand.add_argument("--name", required=True, help="what to call this agent") + understand.add_argument("--path", required=True, help="where the agent is") + understand.add_argument( + "--kind", default="repo", choices=supported(), help="how the agent is supplied" + ) + understand.add_argument("--out", default=None, help="artifact directory") + understand.add_argument( + "--once", + dest="interactive", + action="store_false", + help="run unattended instead of staying open for corrections", + ) + understand.add_argument("--model", default=DEFAULT_MODEL, help=argparse.SUPPRESS) + understand.set_defaults(run=_understand, interactive=True) + + world = sub.add_parser("build", help="build the world from an agent's contract") + world.add_argument("--name", required=True, help="which agent") + world.add_argument("--out", default=None, help="artifact directory") + world.add_argument( + "--once", + dest="interactive", + action="store_false", + help="run unattended instead of staying open for corrections", + ) + world.set_defaults(run=_build, interactive=True) + + scenarios = sub.add_parser( + "scenarios", help="write the scenarios to test the agent with" + ) + scenarios.add_argument("--name", required=True, help="which agent") + scenarios.add_argument("--out", default=None, help="artifact directory") + scenarios.add_argument( + "--count", + type=int, + default=None, + help="how many scenarios to write (defaults to however many already exist)", + ) + scenarios.add_argument( + "--once", + dest="interactive", + action="store_false", + help="run unattended instead of staying open for corrections", + ) + scenarios.set_defaults(run=_scenarios, interactive=True) + + live = sub.add_parser( + "live", help="run the scenarios against the real agent, as a conversation" + ) + live.add_argument("--name", required=True, help="which agent") + live.add_argument("--out", default=None, help="artifact directory") + live.add_argument( + "--once", + dest="interactive", + action="store_false", + help="run unattended instead of staying open", + ) + live.set_defaults(run=_live, interactive=True) + + runs = sub.add_parser("run", help="run the scenarios and grade what happened") + runs.add_argument("--name", required=True, help="which agent") + runs.add_argument("--out", default=None, help="artifact directory") + runs.add_argument( + "--target", + default="local", + choices=target_kinds(), + help="where the agent under test runs", + ) + runs.add_argument( + "--only", nargs="*", default=None, help="run only these scenarios, by name" + ) + runs.add_argument("--model", default=None, help="model for the run") + runs.add_argument( + "--quiet", + action="store_true", + help="only the verdicts, without the conversations as they happen", + ) + runs.set_defaults(run=_run) + + chat = sub.add_parser( + "chat", + help="one conversation: understand, build the world, write the scenarios", + ) + # Nothing is required. Which agent, where it lives and how many scenarios are all things + # you say; naming one here is a shortcut back into work already in progress. + chat.add_argument("--name", default=None, help=argparse.SUPPRESS) + chat.add_argument("--path", default=None, help=argparse.SUPPRESS) + chat.add_argument( + "--kind", default="repo", choices=supported(), help=argparse.SUPPRESS + ) + chat.add_argument("--out", default=None, help=argparse.SUPPRESS) + chat.set_defaults(run=_chat) + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + if getattr(args, "run", None) is None: + args = parser.parse_args([*(argv or []), "chat"]) + return asyncio.run(args.run(args)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/harness/src/agent_harness/config.py b/harness/src/agent_harness/config.py new file mode 100644 index 0000000..bcd3b99 --- /dev/null +++ b/harness/src/agent_harness/config.py @@ -0,0 +1,221 @@ +"""Session configuration for the harness. + +One place decides which model runs, how the session reaches it, and what the agent is allowed to +touch. Every stage builds its options from here so that a change of provider or model is one +edit rather than a search across stages. + +Credentials are never read from source. The Vertex project and credential path come from the +environment, which is also how the rest of the platform resolves them. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any, Iterable + +from claude_agent_sdk import ClaudeAgentOptions + +DEFAULT_MODEL = "claude-sonnet-4-6" + +SKILLS_ROOT = Path(__file__).parent / "skills" +PROJECT_ROOT = Path(__file__).resolve().parents[2] +ARTIFACTS_ROOT = PROJECT_ROOT / "artifacts" + +_READ_ONLY_TOOLS = ("Read", "Glob", "Grep") + + +def credentials_hint() -> str: + """A line saying which credentials a run will use, or a warning that it is guessing. + + Claude Code falls back to the active gcloud login when no service-account file is named, + which is a legitimate setup and an easy accident. The accident produces a provider auth + error several layers down, so it is worth saying out loud which one is in play. + """ + named = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") + if named: + return f"credentials: {Path(named).name}" + return ( + "credentials: none named, falling back to your gcloud login. If calls fail to " + "authenticate, load the env file first:\n" + " set -a; . ./.env.acceptance; set +a" + ) + + +def chosen_model(model: str | None = None) -> str: + """The model a session will actually run on. + + Passed to the session explicitly as well as through the environment. The environment alone + does not win: the CLI has its own default and will quietly use it, so a run meant for Haiku + goes out on whatever the CLI felt like and the bill says so afterwards. + """ + return model or os.environ.get("ALK_HARNESS_MODEL", DEFAULT_MODEL) + + +def provider_env(model: str | None = None) -> dict[str, str]: + """The provider block passed to the session. + + Claude Code resolves the GCP project from ``GOOGLE_CLOUD_PROJECT``, the credential file, or + the active gcloud configuration, in that order, so an unset project id is not an error here. + """ + env = { + "CLAUDE_CODE_USE_VERTEX": "1", + "CLOUD_ML_REGION": os.environ.get("CLOUD_ML_REGION", "global"), + "ANTHROPIC_MODEL": chosen_model(model), + } + for passthrough in ( + "ANTHROPIC_VERTEX_PROJECT_ID", + "GOOGLE_CLOUD_PROJECT", + "GOOGLE_APPLICATION_CREDENTIALS", + ): + value = os.environ.get(passthrough) + if value: + env[passthrough] = value + return env + + +def read_only_session( + *, + system_prompt: str, + cwd: str | Path, + mcp_servers: dict[str, Any] | None = None, + extra_tools: Iterable[str] = (), + max_turns: int = 40, + model: str | None = None, +) -> ClaudeAgentOptions: + """A session that may read the agent under test but never write to it. + + The agent under test is somebody's real repository. The harness reads it and writes its own + artifacts elsewhere, so the built-in write tools are simply not granted; the only way this + session can produce anything is by calling one of ours. + """ + allowed = [*_READ_ONLY_TOOLS, "AskUserQuestion", *extra_tools] + options = ClaudeAgentOptions( + system_prompt=system_prompt, + allowed_tools=allowed, + mcp_servers=dict(mcp_servers or {}), + # Not acceptEdits: that auto-approves Edit and Write before the permission callback + # is consulted, which silently defeats the gate below. + permission_mode="default", + cwd=str(cwd), + setting_sources=[], + max_turns=max_turns, + model=chosen_model(model), + env=provider_env(model), + ) + options.disallowed_tools = list(UNWANTED) + options.hooks = gate_hooks(allowed) + options.can_use_tool = permission_gate(granted=allowed) + return options + + +# Tools the host offers every session that no stage of this harness has any use for. Denying +# them at the gate works and is the backstop, but a denial still costs the turn that discovered +# it — and these get reached for in almost every stage. Naming them as disallowed keeps them out +# of the tool list the model is shown, so the turn is never spent. +UNWANTED = ("ToolSearch", "Bash", "Write", "Edit", "NotebookEdit", "WebFetch", "WebSearch") + + +def gate_hooks(granted: Iterable[str]) -> dict[str, Any]: + """Deny anything a stage was not given, at the point the SDK actually asks. + + ``can_use_tool`` alone does not do this. An ``allowed_tools`` entry approves those tools + before the callback is consulted, and the SDK then warns that the callback is shadowed — so + the gate never runs for the tools we granted, and in practice does not stop the ones we did + not either. A host ``ToolSearch`` reached every stage, returned nothing, and cost a turn each + time. + + A PreToolUse hook is consulted for every call, which is what the deny-by-default rule needed + in order to be true rather than intended. + """ + from claude_agent_sdk.types import HookMatcher + + permitted = {*granted, "AskUserQuestion"} + + async def refuse(payload: dict[str, Any], _tool_use_id: Any, _context: Any) -> dict[str, Any]: + name = str(payload.get("tool_name") or "") + if not name or name in permitted: + return {} + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": ( + f"{name} is not part of this stage. You have " + f"{', '.join(sorted(permitted)) or 'no other tools'}, and everything you " + "produce goes through those, because those are what check it." + ), + } + } + + return {"PreToolUse": [HookMatcher(hooks=[refuse])]} + + +def permission_gate(ask: Any | None = None, granted: Iterable[str] = ()) -> Any: + """Decide what a stage may do: nothing it was not given. + + Deny by default, not deny-a-list. A session is offered whatever tools its host happens to + expose, and anything not named here is by definition not part of how this stage works. An + allow-by-default gate let a host search tool through, which returned nothing useful and cost + a stage its entire turn budget looping on it; the same hole would let a file write through. + + Tools granted through ``allowed_tools`` are approved before this is consulted, so this only + ever sees the ones that were not. + """ + permitted = set(granted) + + async def gate(tool_name: str, payload: dict[str, Any], context: Any) -> Any: + from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny + + if tool_name == "AskUserQuestion" and ask is not None: + return await ask(tool_name, payload, context) + if tool_name in permitted: + return PermissionResultAllow(updated_input=payload) + return PermissionResultDeny( + message=( + f"{tool_name} is not part of this stage. You have " + f"{', '.join(sorted(permitted)) or 'no other tools'}, and everything you " + "produce goes through those, because those are what check it." + ) + ) + + return gate + + +def artifact_dir(agent: str, root: str | Path | None = None) -> Path: + """The folder holding one conversation: its contract, world, scenarios and runs. + + One conversation, one directory. Everything about testing one agent lives together, which is + what makes a session something you can close, reopen, hand over or delete as one thing. + """ + base = Path(root) if root else ARTIFACTS_ROOT / "sessions" + return base / agent + + +HARNESS = SKILLS_ROOT / "harness.md" + + +def load_skill(name: str) -> str: + """One stage's instructions, behind what the harness as a whole is for. + + Every stage gets the same opening: what this harness produces, why the division between what + a model decides and what code decides exists, and what makes a result worth believing. A + stage that knows only its own step does its step well and still gets the point of it wrong — + it works around a gate instead of fixing what the gate named, or it reports a number that + quietly skipped half its checks. + + The stage's own method follows. Both are files, so how any of this works can be changed + without touching code. + """ + path = SKILLS_ROOT / name / "SKILL.md" + if not path.exists(): + raise FileNotFoundError(f"no skill at {path}") + stage = path.read_text(encoding="utf-8") + if not HARNESS.exists(): + return stage + return ( + f"{HARNESS.read_text(encoding='utf-8')}\n\n" + "---\n\n" + "# The stage you are in now\n\n" + f"{stage}" + ) diff --git a/harness/src/agent_harness/contract.py b/harness/src/agent_harness/contract.py new file mode 100644 index 0000000..2a865cf --- /dev/null +++ b/harness/src/agent_harness/contract.py @@ -0,0 +1,442 @@ +"""The agent contract: what the agent verifiably is, read from its own source. + +Everything downstream is confined to this. A world may only implement tools listed here, a +scenario may only reference values grounded in here, and a checkpoint may only assert against +what is here. It is the anti-hallucination device for every later stage. + +The harness produces it by reading the agent's code and calling ``submit_contract``. Validation +runs inside that tool, so problems are returned into the conversation and the model tries again +rather than a bad contract reaching disk. +""" + +from __future__ import annotations + +import json +from typing import Any + +from pydantic import BaseModel, Field, model_validator + +# How a person reaches an agent. This decides how it is later run — voice goes out as a live +# call, everything else runs locally — so it is defined once and referenced, never retyped. +MODALITIES = ("voice", "chat", "browser") + +_STRING_FIELDS = ( + "agent", + "one_liner", + "modality", + "system_prompt_excerpt", + "notes", +) +_LIST_FIELDS = ( + "hard_constraints", + "real_use_cases", + "amendments", +) +_DICT_FIELDS = ("data_schema", "base_environment") + +# What each field gets called when it is not called what we call it. Every one of these was +# written by a model that had read the schema and still reached for the more obvious word. +_ALIASES = { + "real_use_cases": ("use_cases", "usecases", "scenarios", "capabilities"), + "hard_constraints": ("constraints", "rules", "policies", "policy", "guardrails"), + "system_prompt_excerpt": ("system_prompt", "prompt", "instructions"), + "base_environment": ("data", "seed_data", "starting_data", "records"), + "data_schema": ("schema", "record_schema", "data_shape"), + "agent": ("name", "agent_name"), + "one_liner": ("summary", "description"), + "notes": ("observations", "remarks"), +} + + +class ToolSpec(BaseModel): + """One tool the agent really has. + + ``args`` is the load-bearing field: the world's handlers, the probes and every scenario are + built from these exact names. It is also the one most often written under another name — + ``parameters``, ``arguments``, ``params`` — or left out while ``arg_types`` names every + argument anyway. All of those are the same information, so they are accepted and normalised + rather than rejected, because a contract bounced for a synonym costs a full turn and teaches + nothing about the agent. + """ + + @model_validator(mode="before") + @classmethod + def _normalize_args(cls, payload: Any) -> Any: + if not isinstance(payload, dict): + return payload + if not payload.get("args"): + for alias in ("parameters", "arguments", "params", "arg_names"): + value = payload.get(alias) + if isinstance(value, list) and value: + payload["args"] = value + break + # Some writers give {name: type} where a list was asked for. The keys are the + # argument names, which is exactly what was wanted. + if isinstance(value, dict) and value: + payload["args"] = list(value) + payload.setdefault( + "arg_types", {k: str(v) for k, v in value.items()} + ) + break + if not payload.get("args"): + # Nothing named the arguments directly, but a per-argument map still names them. + for source in ("arg_types", "arg_values"): + mapping = payload.get(source) + if isinstance(mapping, dict) and mapping: + payload["args"] = list(mapping) + break + if isinstance(payload.get("args"), str): + payload["args"] = [payload["args"]] + if isinstance(payload.get("args"), list): + payload["args"] = [str(one) for one in payload["args"]] + return payload + + name: str + args: list[str] = Field(default_factory=list) + arg_types: dict[str, str] = Field(default_factory=dict) + arg_values: dict[str, Any] = Field(default_factory=dict) + description: str = "" + + +class ToolEntry(BaseModel): + """How to reach the agent's own implementation of one tool. + + Recorded rather than assumed, because there is no shape every agent shares. A benchmark + writes static methods on a class; a framework agent writes closures inside ``__init__`` that + cannot be imported at all. What the environment does about a tool is decided from ``mode``, + so a tool nobody can reach is visible here rather than quietly reimplemented. + """ + + tool: str + # import: a module-level callable. construct: a method needing an instance built first. + # service: reachable over HTTP. generate: no implementation exists, so the harness writes one. + mode: str = "generate" + module: str = "" + callable: str = "" + # An expression that builds the object a `construct` tool hangs off. + factory: str = "" + # What the agent's own state is passed as, where a tool takes it as an argument. + first_arg: str = "" + notes: str = "" + + +class DataStore(BaseModel): + """What the agent's tools read and write, and how to be there instead of it. + + Nothing recorded here is a change to the agent. It is what the agent **already expects**, + written down so the environment can be built to match: the same host, the same port, the same + database, the same user. Where it reads a value from configuration we set that configuration; + where it hardcodes one we shape our own store to it, which is why a hardcoded value is worth + recording rather than treated as a dead end. + + That inversion is the point. The alternative, editing the agent until it points at us, means + testing something other than what ships. + """ + + # Read off the agent, never chosen for it. Postgres and ClickHouse disagree about dialect, + # types and what a transaction even means, so an agent tested against the wrong one is graded + # on queries it never runs. Free text because the next agent will be on an engine nobody has + # written down yet. + kind: str = "" + version: str = "" + + # The easiest seam, and the one most agents have: one variable or config key holding the whole + # connection string. Set it at launch and nothing else matters. + configured_by: str = "" + config_key: str = "" + + # What the agent expects to find, whether it reads these from config or has them written into + # its source. A hardcoded host is not an obstacle: a network alias makes that name resolve to + # our container, and the agent connects to us believing nothing changed. + host: str = "" + port: int | None = None + database: str = "" + user: str = "" + # Deliberately never the password itself. A contract is written to disk and read by people, so + # a secret in it outlives the run that needed it. What is recorded is where the value comes + # from; if it is genuinely needed it is read at build time and not persisted. + password_from: str = "" + + # An agent that holds its data in memory is reached by calling the function that loads it, not + # by connecting to anything. Recorded so the environment can call the agent's own loader + # rather than reading its files and rebuilding the structure itself, which would be a second + # implementation of the one thing this path exists to stop reimplementing. + schema_from: str = "" + loaded_by: str = "" + loader_module: str = "" + + def has_seam(self) -> bool: + """Whether there is any way to point this agent at our store. + + An agent with no seam at all is a finding, not a thing to work around: it cannot be tested + without one, and saying so is more useful than editing it until it can. + """ + return bool( + self.configured_by + or self.config_key + or self.host + or self.port + or self.database + or self.loader_module + or self.loaded_by + ) + + +class Runtime(BaseModel): + """What it takes to run the agent's code.""" + + language: str = "python" + version: str = "" + install: str = "" + workdir: str = "" + dockerfile: str = "" + + +class Dependency(BaseModel): + """Something the agent reaches for that has to exist before it can work. + + This is what tells the environment stage there is a service to stand up, rather than leaving + it to notice halfway through that a tool has nothing to answer it. The world is a sandbox: + whatever is named here gets built inside it, so the agent's call goes to something real that + happens to be ours. + """ + + name: str + # datastore, service, file, queue — whatever kind of thing this is. Left open rather than + # enumerated, because the next agent will need a kind nobody has thought of yet. + kind: str = "" + what: str = "" + # The tools that cannot work without it. An unreferenced dependency is usually a mistake. + used_by: list[str] = Field(default_factory=list) + + +class AgentContract(BaseModel): + """What the agent verifiably is. Nothing downstream may contradict this.""" + + @model_validator(mode="before") + @classmethod + def _normalize_shapes(cls, payload: Any) -> Any: + """Model JSON varies in benign ways: a list where prose was asked, a bare string where a + list was, a field under the obvious name rather than ours. Normalize instead of + rejecting, because none of that is a grounding error and rejecting it burns turns on + something that does not matter.""" + if not isinstance(payload, dict): + return payload + # The name we chose is not always the obvious one. `real_use_cases` in particular gets + # written as `use_cases`, and the answer it then gets — "no-use-cases" — reads as + # missing rather than misnamed, so the same submission comes back again and again with + # the shape changed and the name untouched. + for ours, others in _ALIASES.items(): + if payload.get(ours): + continue + for other in others: + if payload.get(other): + payload[ours] = payload[other] + break + for key in _STRING_FIELDS: + value = payload.get(key) + if isinstance(value, list): + payload[key] = "\n".join(str(item) for item in value) + elif value is not None and not isinstance(value, str): + payload[key] = str(value) + for key in _LIST_FIELDS: + value = payload.get(key) + if isinstance(value, str): + payload[key] = [value] + elif isinstance(value, list): + payload[key] = [ + str(item) if not isinstance(item, str) else item for item in value + ] + for key in _DICT_FIELDS: + value = payload.get(key) + if value is not None and not isinstance(value, dict): + payload[key] = {"value": value} + return payload + + # Defaulted rather than mandatory so a submission that forgets it reaches validate_contract, + # which says what to do about it, instead of dying in the schema layer with a type error. + agent: str = "" + one_liner: str = "" + modality: str = "chat" + conversational: bool = True + system_prompt_excerpt: str = "" + hard_constraints: list[str] = Field(default_factory=list) + tools: list[ToolSpec] = Field(default_factory=list) + data_schema: dict[str, Any] = Field(default_factory=dict) + base_environment: dict[str, Any] = Field(default_factory=dict) + # What the environment stage has to build before any tool can be answered. + dependencies: list[Dependency] = Field(default_factory=list) + # Whether the agent ships code for its tools: present, absent, or partial. This decides + # whether the environment runs the agent's own tools or writes replacements, and writing a + # replacement where an implementation exists is a defect rather than a choice. + implementation: str = "" + tool_entrypoints: list[ToolEntry] = Field(default_factory=list) + # How this agent's tools say no in a value they return, rather than by raising. Without it a + # refusal cannot be told from a success once the agent's own code is answering the call. + refusal_signature: str = "" + data_store: DataStore | None = None + runtime: Runtime | None = None + real_use_cases: list[str] = Field(default_factory=list) + # Free-form. The fields above are the fixed core because code consumes them; this is where + # the reader records whatever else about *this* agent is worth carrying forward — quirks, + # traps, names that look real but are not — in whatever form fits. It is shown verbatim to + # every later stage. + notes: str = "" + open_questions: list[str] = Field(default_factory=list) + # Anything in here was not read from the agent's source. The contract is meant to be what + # the agent verifiably is, so when the harness widens it the difference is recorded rather + # than blended in, and whoever reads it later can tell the two apart. + amendments: list[str] = Field(default_factory=list) + + def tool_names(self) -> set[str]: + return {tool.name for tool in self.tools} + + def brief(self, *, full_schema: bool = True, with_data: bool = False) -> str: + """The grounding block handed to the model on every downstream call. + + ``with_data`` includes the agent's real starting records rather than only their shape. + A stage that writes scenarios needs to know a menu exists; a stage that builds the world + has to reproduce it row for row, and a shape without records is not enough to do that. + """ + lines: list[str] = [] + for tool in self.tools: + signature = ", ".join( + f"{arg}: {tool.arg_types[arg]}" if arg in tool.arg_types else arg + for arg in tool.args + ) + values = ( + f" [values: {json.dumps(tool.arg_values)[:300]}]" + if tool.arg_values + else "" + ) + lines.append( + f" - {tool.name}({signature}){values} : {tool.description[:140]}" + ) + parts = [ + f"AGENT: {self.agent} - {self.one_liner}", + f"MODALITY: {self.modality}", + "REAL TOOLS (use ONLY these, with these exact arg names and types):\n" + + ("\n".join(lines) or " (none)"), + ] + if self.hard_constraints: + parts.append( + "HARD CONSTRAINTS the agent MUST follow (nothing may contradict these):\n - " + + "\n - ".join(self.hard_constraints[:14]) + ) + if self.data_schema and full_schema: + parts.append( + "DATA SHAPE (the fields each record has):\n" + + json.dumps(self.data_schema)[: 24000 if with_data else 2400] + ) + if self.base_environment and with_data: + parts.append( + "THE AGENT'S REAL STARTING DATA. Reproduce this exactly, including anything\n" + "that looks like a mistake: a misspelled id, an item marked unavailable, an odd\n" + "price. The world is a replica of what the agent has, not a corrected version,\n" + "and a test written against a corrected world will not catch the real bug.\n" + + json.dumps(self.base_environment, ensure_ascii=False) + ) + if self.dependencies: + parts.append( + "WHAT THIS AGENT DEPENDS ON (the environment has to provide each of these):\n - " + + "\n - ".join( + f"{one.name} ({one.kind or 'unspecified'}): {one.what}" + + (f" — used by {', '.join(one.used_by)}" if one.used_by else "") + for one in self.dependencies + ) + ) + if self.real_use_cases: + parts.append( + "REAL USE CASES (what this agent is actually for):\n - " + + "\n - ".join(self.real_use_cases[:12]) + ) + if self.tool_entrypoints: + parts.append( + "THE AGENT'S OWN TOOL CODE. Run these rather than writing replacements:\n - " + + "\n - ".join( + f"{one.tool}: {one.mode}" + + (f" {one.module}.{one.callable}" if one.module else "") + + (f", state passed as {one.first_arg}" if one.first_arg else "") + + (f", build with {one.factory}" if one.factory else "") + for one in self.tool_entrypoints + ) + ) + if self.refusal_signature: + parts.append( + "HOW THIS AGENT REFUSES, in a value rather than by raising:\n " + f"{self.refusal_signature}" + ) + if self.data_store: + store = self.data_store + parts.append( + "ITS DATA STORE:\n" + f" kind: {store.kind or 'unspecified'}\n" + f" connection comes from: {store.configured_by or 'unknown'}\n" + f" schema from: {store.schema_from or 'unknown'}\n" + f" its own loader: {store.loaded_by or 'none'}" + ) + if self.runtime: + run = self.runtime + parts.append( + "RUNNING ITS CODE:\n" + f" {run.language} {run.version}, install with {run.install or 'unknown'}" + + (f", imports resolve from {run.workdir}" if run.workdir else "") + + (f", its own Dockerfile at {run.dockerfile}" if run.dockerfile else "") + ) + if self.notes: + parts.append(f"NOTES from reading the agent:\n{self.notes[:1500]}") + return "\n\n".join(parts) + + def entry_for(self, tool: str) -> ToolEntry | None: + for one in self.tool_entrypoints: + # Coerced rather than assumed. Assigning this field directly bypasses validation, so + # an entry can arrive as a plain mapping, and reading it as an object would raise + # somewhere far from the assignment. + found = one if isinstance(one, ToolEntry) else ToolEntry(**dict(one)) + if found.tool == tool: + return found + return None + + def adoptable(self, tool: str) -> bool: + """Whether this tool has code of its own that should be run instead of replaced.""" + found = self.entry_for(tool) + return bool(found and found.mode in ("import", "construct", "service")) + + +def validate_contract(contract: AgentContract) -> list[str]: + """Structural problems that make a contract unusable downstream. + + Deliberately narrow. This cannot tell whether the model read the agent correctly, only + whether the result is shaped well enough to build a world from. Semantic grounding is the + operator's job, which is why the harness surfaces the contract for review. + """ + problems: list[str] = [] + if not contract.agent.strip(): + problems.append("empty:agent") + if not contract.tools: + problems.append("no-tools") + for index, tool in enumerate(contract.tools): + if not tool.name.strip(): + problems.append(f"tool[{index}]:no-name") + continue + unknown = sorted(set(tool.arg_types) - set(tool.args)) + if unknown: + problems.append( + f"tool[{tool.name}]:types-for-unknown-args:{','.join(unknown)}" + ) + # A tool genuinely taking no arguments is ordinary; every tool taking none is not. It means + # the arguments were read and then not recorded, and since the world, the probes and the + # checkpoints are all built from these names, nothing downstream can detect their absence. + if contract.tools and not any(tool.args for tool in contract.tools): + problems.append( + "no-arguments-on-any-tool: list each tool's exact parameter names in args" + ) + if not contract.real_use_cases: + problems.append("no-use-cases") + # Iterate the tools, not tool_names(): that returns a set, so duplicates collapse before + # they can be counted and the check silently never fires. + names = [tool.name for tool in contract.tools if tool.name.strip()] + duplicates = sorted({name for name in names if names.count(name) > 1}) + if duplicates: + problems.append(f"duplicate-tool-names:{','.join(duplicates)}") + return problems diff --git a/harness/src/agent_harness/environment.py b/harness/src/agent_harness/environment.py new file mode 100644 index 0000000..9d6dbe9 --- /dev/null +++ b/harness/src/agent_harness/environment.py @@ -0,0 +1,67 @@ +"""The small ``fi.simulate.environment`` contract a generated world implements. + +The parent package currently imports optional LiveKit code from ``fi.simulate.__init__`` before a +submodule can be imported. Keep the normal integration when that import is available, while +letting the standalone harness run its deterministic world and scenario gates from the base +editable dependency alone. +""" + +from __future__ import annotations + +from abc import ABC +from typing import Any, Mapping + +try: + from fi.simulate.environment import ( + EnvironmentAdapter, + EnvironmentSnapshot, + ToolExecutionResult, + ) +except ImportError: # pragma: no cover - exercised only without the parent's optional extras + from pydantic import BaseModel, Field + + class EnvironmentSnapshot(BaseModel): + """State and tool definitions published by a local environment.""" + + tools: list[dict[str, Any]] = Field(default_factory=list) + artifacts: list[Any] = Field(default_factory=list) + events: list[Any] = Field(default_factory=list) + state: dict[str, Any] = Field(default_factory=dict) + metadata: dict[str, Any] = Field(default_factory=dict) + + class ToolExecutionResult(BaseModel): + """Result from executing one local tool call.""" + + tool_call_id: str | None = None + tool_name: str + content: str + result: Any = None + success: bool = True + error: str | None = None + state_updates: dict[str, Any] = Field(default_factory=dict) + artifacts: list[Any] = Field(default_factory=list) + events: list[Any] = Field(default_factory=list) + metadata: dict[str, Any] = Field(default_factory=dict) + + def to_tool_message(self) -> dict[str, Any]: + return { + "role": "tool", + "tool_call_id": self.tool_call_id or self.tool_name, + "content": self.content, + } + + class EnvironmentAdapter(ABC): + """The subset of the parent environment protocol the harness needs.""" + + name = "environment" + + def reset(self, **_context: Any) -> EnvironmentSnapshot: + return EnvironmentSnapshot() + + def observe(self, **_context: Any) -> EnvironmentSnapshot: + return EnvironmentSnapshot() + + def handle_tool_call( + self, tool_call: Mapping[str, Any], **_context: Any + ) -> ToolExecutionResult | None: + return None diff --git a/harness/src/agent_harness/folder.py b/harness/src/agent_harness/folder.py new file mode 100644 index 0000000..c278b13 --- /dev/null +++ b/harness/src/agent_harness/folder.py @@ -0,0 +1,220 @@ +"""A scenario as a folder of files, and running the code inside it. + +A scenario used to be a row in one big JSON file, and its setup was a list of rows to insert. +That was enough while every world was a database. It stopped being enough the moment a world +could hold a service as well as a table: "the weather service starts returning errors" is not +expressible as rows, and neither is "the file is missing" or "the queue is backed up". + +So a scenario owns a folder, and the parts that are logic are files: + + scenarios// + scenario.json what it is: instruction, solution, which sub-goals + setup.py def setup(world) — the changes this scenario makes + ready.py def ready(world) — is the world ready for this scenario + checks/.py def check(world, calls) — one per deterministic sub-goal + +The files are the artifact, not a rendering of one. Each is executable on its own, so a check +can be run by hand against what a run left behind and answer exactly what it answers inside the +harness. That is the whole point of them being files: something you can open, read and run is +something you can argue with. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from .catalogue import Catalogue +from .scenario import Scenario +from .world.runtime import GeneratedWorld + +SCENARIOS = "scenarios" +INDEX = "scenarios.json" + +# Appended to every check file the harness writes. The model writes only ``check(world, calls)``; +# this is what makes that same file runnable by a person, so nobody has to keep two versions of +# one truth in step. +_RUNNABLE = ''' + +if __name__ == "__main__": + # Run this check by hand against what a run left behind. The first argument is anything + # inside the saved world's folder, because not every world has a database to name: + # python /manifest.json [calls.json] + import json as _json + import sys as _sys + from pathlib import Path as _Path + + from agent_harness.world.runtime import Call as _Call + from agent_harness.world.snapshot import restore as _restore + + _world = _restore(_Path(_sys.argv[1]).parent) if len(_sys.argv) > 1 else None + _calls = [] + if len(_sys.argv) > 2: + _calls = [_Call(**_one) for _one in _json.loads(_Path(_sys.argv[2]).read_text())] + _said = check(_world, _calls) + print("held" if _said is None else f"FAILED: {_said}") + raise SystemExit(0 if _said is None else 1) +''' + + +@dataclass +class Outcome: + """What one piece of a scenario's own code did.""" + + ok: bool + said: str = "" + broken: bool = False + + +def _run(source: str, name: str, entry: str, *args: Any) -> Outcome: + """Execute one function out of a scenario's own code. + + A file that will not compile, or that raises, is **broken** rather than failing: it is our + mistake, and scoring it as though the world were wrong would send somebody looking in the + wrong place. + """ + if not source.strip(): + return Outcome(True) + namespace: dict[str, Any] = {} + try: + exec(compile(source, f"<{name}>", "exec"), namespace) + except Exception as failed: + return Outcome(False, f"{name} would not compile: {failed}", broken=True) + + function = namespace.get(entry) + if not callable(function): + return Outcome(False, f"{name} defines no {entry}()", broken=True) + try: + said = function(*args) + except Exception as failed: + return Outcome( + False, f"{name} raised {type(failed).__name__}: {failed}", broken=True + ) + # The convention is that a complaint is a sentence, and anything else means it held. An empty + # string is the case worth naming: it reads as "no complaint" to whoever wrote it, and taking + # it as a failure produces a rejection with no reason attached, which cannot be acted on and + # sends the author hunting for a problem that is not there. + if said is None or said is True or (isinstance(said, str) and not said.strip()): + return Outcome(True) + if said is False: + return Outcome( + False, + f"{name} returned False without saying what is wrong. Return the sentence instead, " + "or None if it holds.", + ) + return Outcome(False, str(said)) + + +def apply_setup(scenario: Scenario, world: GeneratedWorld) -> Outcome: + """Make this scenario's changes to the world.""" + return _run(scenario.setup_code, f"{scenario.name}/setup.py", "setup", world) + + +def check_ready(scenario: Scenario, world: GeneratedWorld) -> Outcome: + """Whether the world now holds what this scenario presumes.""" + return _run(scenario.ready_code, f"{scenario.name}/ready.py", "ready", world) + + +def folder_for(destination: Path, name: str) -> Path: + return Path(destination) / SCENARIOS / name + + +def write_folder(scenario: Scenario, catalogue: Catalogue, destination: Path) -> Path: + """Write one scenario out as its own folder of files.""" + root = folder_for(destination, scenario.name) + (root / "checks").mkdir(parents=True, exist_ok=True) + + body = scenario.model_dump() + # The code lives in its own files; keeping a second copy in the JSON would let the two drift + # and leave nobody able to say which one ran. + body.pop("setup_code", None) + body.pop("ready_code", None) + (root / "scenario.json").write_text( + json.dumps(body, indent=2, ensure_ascii=False), encoding="utf-8" + ) + + (root / "setup.py").write_text( + scenario.setup_code + or "def setup(world):\n \"\"\"This scenario runs on the base world unchanged.\"\"\"\n", + encoding="utf-8", + ) + (root / "ready.py").write_text( + scenario.ready_code + or "def ready(world):\n \"\"\"Nothing beyond the base world is presumed.\"\"\"\n", + encoding="utf-8", + ) + + for name in scenario.sub_goals: + sub_goal = catalogue.named(name) + if sub_goal is None or not sub_goal.deterministic(): + continue + (root / "checks" / f"{name}.py").write_text( + sub_goal.check.rstrip() + "\n" + _RUNNABLE, encoding="utf-8" + ) + return root + + +def read_folder(destination: Path, name: str) -> Scenario | None: + """One scenario, reassembled from its folder.""" + root = folder_for(destination, name) + body = root / "scenario.json" + if not body.exists(): + return None + payload = json.loads(body.read_text(encoding="utf-8")) + for field, filename in (("setup_code", "setup.py"), ("ready_code", "ready.py")): + path = root / filename + payload[field] = path.read_text(encoding="utf-8") if path.exists() else "" + return Scenario.model_validate(payload) + + +def write_index(scenarios: list[Scenario], destination: Path) -> Path: + """The whole suite at a glance, over the folders. + + Regenerated from the folders rather than maintained alongside them, so it can never disagree + with what is actually on disk. + """ + destination = Path(destination) + destination.mkdir(parents=True, exist_ok=True) + path = destination / INDEX + path.write_text( + json.dumps( + [ + { + "name": one.name, + "use_case": one.use_case, + "tests": one.tests, + "instruction": one.instruction, + "sub_goals": one.sub_goals, + "steps": len(one.solution), + "folder": f"{SCENARIOS}/{one.name}", + } + for one in scenarios + ], + indent=2, + ensure_ascii=False, + ), + encoding="utf-8", + ) + return path + + +def read_all(destination: Path) -> list[Scenario]: + """Every scenario on disk, read from the folders.""" + root = Path(destination) / SCENARIOS + if not root.exists(): + return [] + found: list[Scenario] = [] + for folder in sorted(root.iterdir()): + if not folder.is_dir(): + continue + try: + scenario = read_folder(destination, folder.name) + except Exception: + # A folder we cannot read is skipped rather than crashing the stage: the rest of the + # suite is still usable, and the gap shows up as a missing scenario. + continue + if scenario is not None: + found.append(scenario) + return found diff --git a/harness/src/agent_harness/prove.py b/harness/src/agent_harness/prove.py new file mode 100644 index 0000000..ce5b6f9 --- /dev/null +++ b/harness/src/agent_harness/prove.py @@ -0,0 +1,217 @@ +"""Proving a scenario is worth keeping, before anything is ever run against the agent. + +Three gates, all pure code. No model is asked whether a scenario is good; the environment +decides. Terminal-bench keeps its tasks honest this way, and it is the cheapest useful thing in +the whole harness: no tokens, no network, a few milliseconds. + +**Ready.** Reset the world, run the scenario's own ``setup.py``, then its ``ready.py``. The world +has to hold what the scenario presumes. A scenario about the last five chocolates is only a test +of the agent if there really are five; otherwise the agent fails for something we got wrong and +it reads as the agent's fault. This gate is why a missing precondition can never be mistaken for +a finding. + +**Solvable.** Then run the reference solution and the checks. They must pass. If they do not, +either the scenario cannot be passed at all or its checks are wrong, and both have happened +here: one scenario asserted a value the agent was never permitted to send; another demanded +confirmation of an item that could not be ordered. Neither was noticed until a live run failed +and read as a finding about the agent. + +**Not vacuous.** Then reset, set up again, run *nothing*, and run the checks. They must fail. A +check that passes with no actions taken grades nothing while reporting a result, which is how a +suite goes quietly green. This one earns its keep: on a third-party benchmark it caught three +sub-goals that passed trivially because the seeded world already contained a cancelled order. + +Only a scenario that clears all three is kept. That is the green light. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +from .checks import Outcome, run_check +from .catalogue import Catalogue +from .folder import apply_setup, check_ready +from .scenario import Scenario +from .world.runtime import Call, GeneratedWorld +from .world.snapshot import restore + + +@dataclass +class Proof: + """Whether a scenario holds up, and what happened when it was tried.""" + + ready: bool = False + solvable: bool = False + vacuous: bool = True + why_not_ready: str = "" + # Checks that held with nothing done. The scenario is only vacuous when *every* check does + # that, but a single one still grades nothing, and since sub-goals are shared it will report + # itself as held for an agent that did nothing at all. Named rather than refused: on a + # scenario about a refusal, "no order was placed" holding on an untouched world is correct. + weak: list[str] = field(default_factory=list) + with_solution: list[Outcome] = field(default_factory=list) + with_nothing: list[Outcome] = field(default_factory=list) + refused: list[str] = field(default_factory=list) + broken: list[str] = field(default_factory=list) + + @property + def holds(self) -> bool: + return self.ready and self.solvable and not self.vacuous and not self.broken + + def gates(self) -> dict[str, bool]: + """The three answers, for anything that wants to show them.""" + return { + "ready": self.ready, + "solvable": self.solvable, + "not_vacuous": not self.vacuous, + } + + def why(self) -> str: + """What to fix, in the order worth fixing it.""" + if not self.ready: + return ( + "the world is not ready for this scenario, so running it would test us rather " + f"than the agent:\n - {self.why_not_ready}\n\n" + "Either setup.py does not make the change this scenario needs, or ready.py is " + "checking for something the setup never creates." + ) + if self.broken: + return "these checks are broken, not failing:\n - " + "\n - ".join( + self.broken + ) + if not self.solvable: + failed = [one for one in self.with_solution if not one.held] + said = "\n - ".join(f"{one.name}: {one.said}" for one in failed) + refusals = ( + "\n\nThe solution's own calls were refused by the world:\n - " + + "\n - ".join(self.refused) + if self.refused + else "" + ) + return ( + "the reference solution does not pass this scenario's own checks, so either the " + "scenario cannot be passed or the checks are wrong:\n - " + said + refusals + ) + if self.vacuous: + passed = [one.name for one in self.with_nothing if one.held] + return ( + "these checks pass without the agent doing anything, so they grade nothing:\n - " + + "\n - ".join(passed) + + "\n\nIf the point of this scenario is that nothing should happen, checking " + "the world alone cannot show it — an untouched world looks identical to one " + "where the agent did nothing at all. Check the calls instead: that the agent " + "tried, and that the attempt was refused rather than succeeding.\n" + " def check(world, calls):\n" + " tried = [c for c in calls if c.name == 'add']\n" + " if not tried: return 'never attempted it'\n" + " if any(c.ok for c in tried): return 'it succeeded'\n" + " return None" + ) + return "holds" + + +def _checks_for(scenario: Scenario, catalogue: Catalogue) -> list[tuple[str, str]]: + """The deterministic checks this scenario is graded by, in catalogue order.""" + chosen: list[tuple[str, str]] = [] + for name in scenario.sub_goals: + sub_goal = catalogue.named(name) + if sub_goal is not None and sub_goal.deterministic(): + chosen.append((name, sub_goal.check)) + return chosen + + +def prepared( + scenario: Scenario, world_root: Path +) -> tuple[GeneratedWorld, Outcome, Outcome]: + """A fresh world with this scenario's setup applied, and how that went.""" + world = restore(world_root) + world.reset() + applied = apply_setup(scenario, world) + ready = check_ready(scenario, world) if applied.ok else Outcome(False, applied.said) + # The setup's own calls are not the agent's. Clearing them keeps a check that counts calls + # from crediting the agent with work the scenario did on its behalf. + world.calls = [] + return world, applied, ready + + +def _run( + scenario: Scenario, world_root: Path, *, with_solution: bool +) -> tuple[GeneratedWorld, list[Call], list[str]]: + """A world set up for this scenario, optionally with the solution played through it.""" + world, _applied, _ready = prepared(scenario, world_root) + refused: list[str] = [] + if with_solution: + for step in scenario.solution: + call = world.call(step.tool, step.arguments) + if not call.ok: + refused.append(f"{call.name}({step.arguments}): {call.error}") + return world, list(world.calls), refused + + +def prove(scenario: Scenario, catalogue: Catalogue, world_root: Path) -> Proof: + """Run all three gates and say whether this scenario is worth keeping.""" + proof = Proof() + checks = _checks_for(scenario, catalogue) + if not checks: + proof.broken = [ + "none of this scenario's sub-goals has a check in code, so nothing here can be " + "settled without asking a model" + ] + return proof + + # Gate 1: is the world ready for this scenario at all? + world, applied, ready = prepared(scenario, world_root) + world.close() + if not applied.ok: + proof.why_not_ready = applied.said + if applied.broken: + proof.broken = [applied.said] + return proof + if not ready.ok: + proof.why_not_ready = ready.said + if ready.broken: + proof.broken = [ready.said] + return proof + proof.ready = True + + # Gate 2: does the reference solution pass this scenario's own checks? + world, calls, refused = _run(scenario, world_root, with_solution=True) + try: + proof.with_solution = [ + run_check(source, world, calls, name=name) for name, source in checks + ] + finally: + world.close() + proof.refused = refused + proof.broken = [one.name for one in proof.with_solution if one.broken] + proof.solvable = all(one.held for one in proof.with_solution) and not proof.broken + + # Gate 3: do those same checks fail when nothing is done? + untouched, nothing, _ = _run(scenario, world_root, with_solution=False) + try: + proof.with_nothing = [ + run_check(source, untouched, nothing, name=name) for name, source in checks + ] + finally: + untouched.close() + # Vacuous only if *every* check still passes with nothing done. One check that survives an + # empty run is often legitimate — "no order was placed" is a real thing to assert about a + # refusal scenario — but a whole set of them means nothing is being graded. + proof.weak = [one.name for one in proof.with_nothing if one.held] + # A judged sub-goal reads what the agent said, and an agent that did nothing said nothing, so + # it cannot be passed by an empty run the way a state check can. That matters for a whole + # legitimate class of scenario: where the right behaviour is to decline and touch nothing, + # every check about the world holds vacuously and the explanation is the only real evidence. + # Without this the gate rejects exactly the scenarios that test a refusal. + judged = [ + name + for name in scenario.sub_goals + if (found := catalogue.named(name)) is not None and not found.deterministic() + ] + proof.vacuous = ( + bool(proof.with_nothing) + and len(proof.weak) == len(proof.with_nothing) + and not judged + ) + return proof diff --git a/harness/src/agent_harness/reception.py b/harness/src/agent_harness/reception.py new file mode 100644 index 0000000..880806e --- /dev/null +++ b/harness/src/agent_harness/reception.py @@ -0,0 +1,162 @@ +"""Stage zero: working out which agent you mean. + +Everything the harness does is about one agent, so something has to establish which one. That +used to be two flags on a command line, which is the wrong place for it: the whole point is that +you say what you want and it happens, and "here is my agent, set up a test environment for it" +is a sentence, not an invocation. + +So this is a stage like any other. It can look around the filesystem to find what you are +pointing at, it asks if what you said is ambiguous, and it finishes by naming the agent and where +it lives. Everything after it, including where artifacts are written, follows from that. +""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from claude_agent_sdk import ClaudeAgentOptions, create_sdk_mcp_server, tool + +from .config import ( + UNWANTED, + artifact_dir, + chosen_model, + gate_hooks, + permission_gate, + provider_env, +) +from .session import Stage +from .sources import AgentSource, clone_github_repository, resolve, supported +from .tools import qualified, schema + +RECEPTION_SERVER = "agent" +TOOL_NAMES = ("point_at_agent",) + +_INSTRUCTIONS = """ +You are the front desk of a harness that builds test environments for agents. + +Somebody has arrived with an agent they want tested. Your only job is to work out which agent, +and where it lives, and then call point_at_agent. Nothing else happens until you do. + +Usually they will just tell you: a path, a repository, a folder, or a public GitHub URL. Take it. +For a local path, use Read, Glob and Grep to check it exists and to pick a sensible short name if +they did not give one. For a GitHub URL, call point_at_agent with kind "github" and the URL as +path. The harness clones it into this session; do not ask them to clone it themselves. A name is a +label for their artifacts, so lower case and no spaces. + +The agent is usually somewhere else on disk, not inside the harness. A path they give you is +relative to where you are looking from, which is a workspace holding many repositories, so try +it as given before deciding it does not exist. + +If the path really is not there, say so and say what you did find near it. If they gestured +vaguely at a directory holding several agents, look, and ask which one with AskUserQuestion. + +Do not read the agent properly and do not start working anything out about it. That is the next +stage's job and it has its own instructions. Point at the agent, say in one line what you are +about to do, and stop. +""" + + +def point_at( + name: str, + path: str, + kind: str, + found: dict[str, AgentSource], + source_dir: Path | None = None, +) -> dict[str, Any]: + """Establish which agent this is, or say why it cannot be. + + A plain function rather than only a tool body, so what counts as a reachable agent can be + exercised without standing up a session. + """ + name, path, kind = name.strip(), path.strip(), (kind.strip() or "repo") + if not name: + return _err("no name: the artifacts have to be filed under something") + if kind not in supported(): + return _err(f"no such kind {kind!r}; there is {', '.join(supported())}") + if kind == "repo" and not Path(path).expanduser().exists(): + return _err( + f"there is nothing at {path!r}. Look again with Glob, and if you cannot find it, " + "ask where the agent actually lives." + ) + try: + if kind == "github": + root = clone_github_repository(path, source_dir or artifact_dir(name) / "source") + found["source"] = resolve(kind, name=name, root=root, url=path) + else: + found["source"] = resolve(kind, name=name, root=Path(path).expanduser()) + except Exception as failed: + return _err(f"could not reach that agent: {failed}") + return { + "content": [{"type": "text", "text": f"Pointed at {name} ({kind}) at {path}."}] + } + + +def open_stage( + *, + cwd: str | Path | None = None, + source_dir: str | Path | None = None, + ask: Callable[..., Any] | None = None, + max_turns: int = 20, +) -> tuple[Stage, dict[str, AgentSource]]: + """A stage that establishes which agent this conversation is about.""" + found: dict[str, AgentSource] = {} + + @tool( + "point_at_agent", + "Name the agent this conversation is about and say where it is. `kind` is how it is " + f"supplied, one of: {', '.join(supported())}. For a repository, `path` is its directory; " + "for github, it is the public HTTPS repository URL and the harness clones it. Call this " + "once you know what you are pointing at.", + schema({"name": str, "path": str, "kind": str}, ["name", "path"]), + ) + async def point_at_agent(args: dict[str, Any]) -> dict[str, Any]: + return point_at( + str(args.get("name") or ""), + str(args.get("path") or ""), + str(args.get("kind") or "repo"), + found, + Path(source_dir) if source_dir else None, + ) + + server = create_sdk_mcp_server( + name=RECEPTION_SERVER, version="0.1.0", tools=[point_at_agent] + ) + allowed = [ + "Read", + "Glob", + "Grep", + "AskUserQuestion", + *(qualified(RECEPTION_SERVER, name) for name in TOOL_NAMES), + ] + options = ClaudeAgentOptions( + system_prompt=_INSTRUCTIONS.strip(), + allowed_tools=allowed, + mcp_servers={RECEPTION_SERVER: server}, + # Not acceptEdits: that auto-approves Edit and Write before the permission callback is + # consulted, so a stage can rewrite an artifact by hand and skip the tool whose + # whole job is to validate that change. + permission_mode="default", + cwd=str(cwd or Path.cwd()), + setting_sources=[], + max_turns=max_turns, + model=chosen_model(), + env=provider_env(), + ) + options.disallowed_tools = list(UNWANTED) + options.hooks = gate_hooks(allowed) + options.can_use_tool = permission_gate(ask, allowed) + return Stage(options, name="reception"), found + + +def opening() -> str: + return ( + "Somebody has just opened the harness and has not said anything yet. Greet them in one " + "short line and ask which agent they want tested and where it lives. Do not list your " + "capabilities." + ) + + +def _err(text: str) -> dict[str, Any]: + return {"content": [{"type": "text", "text": text}], "is_error": True} diff --git a/harness/src/agent_harness/run/__init__.py b/harness/src/agent_harness/run/__init__.py new file mode 100644 index 0000000..019dd38 --- /dev/null +++ b/harness/src/agent_harness/run/__init__.py @@ -0,0 +1,275 @@ +"""Stage four: run the scenarios against the world and say what happened. + +Every scenario gets its own world. It is restored from the frozen snapshot, the scenario's own +setup is run against it, and it is thrown away afterwards. Nothing a scenario does can reach the +next one, which is what makes a result mean something on its own and makes the whole suite +repeatable a week later. + +The shape is the same regardless of what is being tested: restore, converse, grade against the +state that is left behind. Where the agent actually runs is a target, so the same scenarios grade +a hosted agent without any of this changing. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Callable, Sequence + +from ..contract import AgentContract +from ..catalogue import load_catalogue +from ..simulator import load_simulator_prompt +from ..scenario import Scenario +from ..folder import apply_setup, check_ready +from ..world.snapshot import restore +from .conversation import FINISHED, Exchange, Transcript, converse +from .grade import ( + Checkpoint, + Result, + as_json, + checkpoints, + grade_sub_goals, + judge, + judge_suite_evals, + summarise, +) +from .targets import LocalAgent, Target, register_target, resolve, supported + + +def _cases(report: Any) -> list[Any]: + """The per-scenario results, whichever report this is. + + The runner hands back a ``SimulationReport``, whose cases are ``test_cases`` and whose + messages live a level down; the plugins hand back the older ``TestReport``, whose cases are + ``results``. Reading only one of them finds nothing in the other and reports a run in which + nobody said anything, which is indistinguishable from an agent that ignored the person. + """ + legacy = getattr(report, "to_legacy", None) + if callable(legacy): + try: + report = legacy() + except Exception: # noqa: BLE001 - a report that will not convert is still readable + pass + return list(getattr(report, "results", None) or getattr(report, "test_cases", None) or []) + + +def from_alk(report: Any, world, spent: float) -> Transcript: + """What ALK's report says happened, in the shape the grading already reads. + + Read from ``messages``, the normalised trajectory, and not from ``transcript`` — which is a + string, so iterating it yields characters, matches nothing, and produces a run with no turns + at all. The judge is then handed an empty conversation and fails every claim about what was + said, which arrives looking like an agent that never spoke. + """ + exchanges: list[Exchange] = [] + for case in _cases(report): + for message in getattr(case, "messages", None) or []: + if not isinstance(message, dict): + continue + role = str(message.get("role") or "") + text = message.get("content") or "" + # Tool turns are in here too, and they are already recorded as calls. Putting them + # in the conversation as well would have the judge read a tool result as something + # the agent said. + if role in ("tool", "function") or not str(text).strip(): + continue + exchanges.append( + Exchange("agent" if role == "assistant" else "customer", str(text)) + ) + if not exchanges and isinstance(getattr(case, "transcript", None), str): + # A plugin that only fills the text form still has to be readable. + spoken = case.transcript.strip() + if spoken: + exchanges.append(Exchange("customer", spoken)) + return Transcript( + exchanges=exchanges, + calls=list(world.calls), + ended=FINISHED, + spent_usd=spent, + ) + + +def audio_from(report: Any) -> str: + """Where ALK left this run's audio, if it left any. + + Artifacts are how a modality hands back what it produced, so the recording is asked of the + report rather than guessed at from a directory. A run with none says so. + """ + for case in _cases(report): + for artifact in getattr(case, "artifacts", None) or []: + kind = str(getattr(artifact, "type", "") or "") + mime = str(getattr(artifact, "mime_type", "") or "") + if "audio" in kind.lower() or mime.startswith("audio/"): + found = getattr(artifact, "path", None) or getattr(artifact, "uri", None) + if found: + return str(found) + return "" + + +RUNS = "runs.json" +REPORT = "report.txt" + +__all__ = [ + "Checkpoint", + "Exchange", + "LocalAgent", + "Result", + "Target", + "Transcript", + "converse", + "register_target", + "run_scenario", + "run_suite", + "supported", + "summarise", +] + + +async def run_scenario( + scenario: Scenario, + contract: AgentContract, + world_root: Path, + *, + target: str = "local", + model: str | None = None, + through_alk: bool = False, + on_exchange: Callable[[Exchange], Any] | None = None, +) -> Result: + """Run one scenario in its own copy of the world and grade what it left behind.""" + catalogue = load_catalogue(world_root) + world = restore(world_root) + try: + # reset() is how an environment is started in ALK: it clears the call log and + # publishes the tools and the starting state. Going through it keeps a generated world + # drivable by anything that already drives an environment. + world.reset() + applied = apply_setup(scenario, world) + if not applied.ok: + raise RuntimeError(f"the scenario's setup did not run: {applied.said}") + ready = check_ready(scenario, world) + if not ready.ok: + raise RuntimeError( + f"the world is not ready for this scenario: {ready.said}. Running it would " + "test us rather than the agent." + ) + # The setup's calls are not the agent's. + world.calls = [] + if through_alk: + # ALK owns the simulation and drives the world through EnvironmentAdapter; the + # harness only grades what it is left with. Nothing here is modality-specific, + # which is the point: the browser and voice runners take the same adapter. + from .alk import simulate + + report, spent = await simulate( + scenario, + contract, + world, + model=model, + simulator_prompt=load_simulator_prompt(world_root), + ) + transcript = from_alk(report, world, spent) + for exchange in transcript.exchanges: + if on_exchange: + on_exchange(exchange) + else: + agent = resolve(target)(contract, world, model=model) + transcript = await converse( + agent, + scenario, + contract, + world_root=world_root, + model=model, + on_exchange=on_exchange, + ) + # Settled by code first. The judge is only handed the sub-goals whose catalogue entry + # says nothing observable decides them. + settled = grade_sub_goals(world, scenario, catalogue, transcript.calls) + ending = ", ".join( + f"{name}: {len(rows)} rows" + for name, rows in sorted(world.observe().state.items()) + ) + judgements, judged_cost = await judge( + scenario, transcript, contract, catalogue, model=model, ending=ending + ) + judgements += judge_suite_evals( + catalogue.suite_evals, scenario, transcript, contract, ending=ending + ) + return Result( + scenario=scenario.name, + tests=scenario.tests, + state_failures=[ + f"{one.name}: {one.said}" for one in settled if not one.held + ], + conduct=judgements, + checkpoints=checkpoints(settled, judgements), + crashes=[f"{call.name}: {call.error}" for call in transcript.crashed()], + ended=transcript.ended, + turns=len(transcript.exchanges), + calls=len(transcript.calls), + spent_usd=transcript.spent_usd + judged_cost, + transcript=transcript.spoken(), + actions=transcript.actions(), + ) + finally: + world.close() + + +async def run_suite( + scenarios: Sequence[Scenario], + contract: AgentContract, + world_root: Path, + *, + target: str = "local", + model: str | None = None, + through_alk: bool = False, + out: Path | None = None, + on_result: Callable[[Result], Any] | None = None, + on_exchange: Callable[[Exchange], Any] | None = None, +) -> list[Result]: + """Run every scenario and write the results out. One failing scenario never stops the rest.""" + destination = Path(out or world_root) + results: list[Result] = [] + for scenario in scenarios: + try: + result = await run_scenario( + scenario, + contract, + world_root, + target=target, + model=model, + through_alk=through_alk, + on_exchange=on_exchange, + ) + except Exception as failed: + # A scenario that could not be run is recorded as unrunnable rather than as a + # failure of the agent, and the rest of the suite still runs. + result = Result( + scenario=scenario.name, + tests=scenario.tests, + crashes=[f"could not run: {type(failed).__name__}: {failed}"], + ended="not-run", + ) + results.append(result) + if on_result: + on_result(result) + + destination.mkdir(parents=True, exist_ok=True) + # Records for scenarios this suite did not run are kept, not clobbered. A live call and a + # local run write to the same file, and re-running two scenarios must not erase the third. + ran = {result.scenario for result in results} + kept = [ + record + for record in load_results(destination) + if isinstance(record, dict) and record.get("scenario") not in ran + ] + merged = kept + json.loads(as_json(results)) + (destination / RUNS).write_text( + json.dumps(merged, indent=2, ensure_ascii=False), encoding="utf-8" + ) + (destination / REPORT).write_text(summarise(results), encoding="utf-8") + return results + + +def load_results(destination: Path) -> list[dict[str, Any]]: + path = Path(destination) / RUNS + return json.loads(path.read_text(encoding="utf-8")) if path.exists() else [] diff --git a/harness/src/agent_harness/run/alk.py b/harness/src/agent_harness/run/alk.py new file mode 100644 index 0000000..2925dae --- /dev/null +++ b/harness/src/agent_harness/run/alk.py @@ -0,0 +1,186 @@ +"""Running a generated world through ALK's own simulation, rather than beside it. + +The whole reason a generated world subclasses ``EnvironmentAdapter`` is so the runners that +already exist can drive it. ``ChatEnvironment`` takes ``environment=`` and owns the +synthetic user, the turn loop, the transcript and the report; the browser and voice paths take +the same adapter. A second loop written here would work for exactly one modality and would have +to be rewritten for the next one, which is the thing this design exists to avoid. + +So the split is: + +- **ALK** drives the simulation: who the customer is, when they speak, when it ends. +- **The world** answers every tool call, through ``handle_tool_call``. +- **The harness** grades afterwards, from the state the world is left in plus the transcript. + +What is written here is only the two adapters between the shapes: a scenario becomes an ALK +``Persona``, and the agent under test becomes an ``AgentWrapper``. +""" + +from __future__ import annotations + +from typing import Any + +from fi.simulate import Persona, Scenario as AlkScenario +from fi.simulate.agent.wrapper import AgentInput, AgentResponse, AgentWrapper +from fi.simulate.environments.chat import ChatEnvironment + +from ..contract import AgentContract +from ..scenario import Scenario +from ..world.runtime import GeneratedWorld +from .targets import LocalAgent + + +def as_persona(scenario: Scenario, simulator_prompt: str = "") -> Persona: + """One of our scenarios, in the shape ALK's simulation consumes. + + ``situation`` is the simulator prompt the harness wrote for this agent with the scenario's + values filled in. ALK wraps it in its own voice-execution rules, so what goes here is only + what changes per scenario, not a second set of instructions about how to behave on a call. + + There is no persona payload beyond a label. Who the caller is does not vary between + scenarios; what varies is what they want and what they know. + """ + from ..simulator import fill + + # Only the circumstance, not the behavioural prompt. ALK composes the opening line as + # "My name is X. {situation} I want this outcome: {outcome}" and sends it verbatim, so + # anything put here is read aloud: with the whole simulator prompt in it, the conversation + # opened with the person reciting their own instructions, including the rules about what + # they are supposed to hold back. + # + # What that costs is real and worth naming: the behaviours the prompt describes — waiting to + # be asked, accepting a refusal once, ending when the answer arrives — are not reaching the + # simulator on this path. Carrying them properly means ALK letting a persona have a briefing + # separate from its first utterance, which is a change to make there rather than to work + # around here. + filled = fill(simulator_prompt, scenario.slots())[0] if simulator_prompt else "" + return Persona( + persona={"name": "customer"}, + situation=scenario.instruction or filled, + # Said in the person's own terms, because it is read aloud with the situation. + # ``scenario.tests`` describes what the suite is checking — "agent correctly counts + # customers filtered by country" — and a person who opens by announcing what the agent + # is being graded on has told it the answer. + outcome="get what you came for, or accept that you cannot", + ) + + +def as_alk_scenario( + scenarios: list[Scenario], name: str = "harness", simulator_prompt: str = "" +) -> AlkScenario: + return AlkScenario( + name=name, + description="generated by the harness", + dataset=[as_persona(one, simulator_prompt) for one in scenarios], + ) + + +def _spoken(input: AgentInput) -> str: + """What the customer just said, as text. + + ALK passes a message as a mapping, not a string, and hands the whole history alongside it. + Passing the mapping straight to a session that expects text fails inside the SDK with a + redacted TypeError, which says nothing about where it came from. + """ + latest = input.new_message + if isinstance(latest, dict): + content = latest.get("content") + if isinstance(content, list): + content = " ".join( + part.get("text", "") for part in content if isinstance(part, dict) + ) + if content: + return str(content) + if isinstance(latest, str) and latest: + return latest + for message in reversed(input.messages or []): + if isinstance(message, dict) and message.get("role") != "assistant": + content = message.get("content") + if content: + return str(content) + return "(the customer said nothing)" + + +class ContractAgent(AgentWrapper): + """The agent under test, in the shape ALK drives agents by. + + It holds the same session ``LocalAgent`` uses, so the agent being graded is identical either + way; what changes is who runs the conversation around it. The tool calls it made are reported + back to ALK so they appear in the transcript, having already gone through the world. + """ + + def __init__( + self, + contract: AgentContract, + world: GeneratedWorld, + *, + model: str | None = None, + ) -> None: + self.agent = LocalAgent(contract, world, model=model) + self.world = world + self._open = False + + async def call(self, input: AgentInput) -> AgentResponse: + if not self._open: + await self.agent.open() + self._open = True + + before = len(self.world.calls) + said = await self.agent.say(_spoken(input)) + made = self.world.calls[before:] + + return AgentResponse( + content=said, + tool_calls=[ + {"name": call.name, "arguments": call.arguments} for call in made + ], + tool_responses=[ + { + "name": call.name, + "content": call.error if not call.ok else str(call.result), + "success": call.ok, + } + for call in made + ], + ) + + async def aclose(self) -> None: + if self._open: + await self.agent.close() + self._open = False + + @property + def spent_usd(self) -> float: + return self.agent.spent_usd + + +async def simulate( + scenario: Scenario, + contract: AgentContract, + world: GeneratedWorld, + *, + model: str | None = None, + simulator_prompt: str = "", +) -> tuple[Any, float]: + """Run one scenario through ALK's chat simulation against this world. + + ``auto_execute_tools`` is off because the agent's tools are bound to the world already and + have run by the time it answers. Turning it on would execute every call a second time, which + for a world that really writes rows means every order placed twice. + """ + agent = ContractAgent(contract, world, model=model) + try: + report = await ChatEnvironment().run( + scenario=as_alk_scenario( + [scenario], name=scenario.name, simulator_prompt=simulator_prompt + ), + agent_callback=agent, + environment=world, + auto_execute_tools=False, + max_turns=max(2, scenario.max_turns), + min_turns=2, + modality=contract.modality or "text", + ) + finally: + await agent.aclose() + return report, agent.spent_usd diff --git a/harness/src/agent_harness/run/call.py b/harness/src/agent_harness/run/call.py new file mode 100644 index 0000000..30b198b --- /dev/null +++ b/harness/src/agent_harness/run/call.py @@ -0,0 +1,102 @@ +"""One scenario, against the real hosted agent, end to end. + +Everything the harness built is wired together here and then ALK's own voice case places the +call. The harness does not reimplement any of that: it supplies the world the agent's tools act +on, the caller's instruction, and the grading afterwards. + + world + setup ──► webhook ──► public url ──► assistant's own tools repointed + │ + ALK's voice case places the call ──┘ + │ + the world afterwards + the calls ──► sub-goal checks + +Run it: + + set -a; . ./.env.acceptance; set +a + uv run python -m harness.run.call --name drive_thru --scenario orders_a_big_mac +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from pathlib import Path + +from ..config import artifact_dir +from ..scenario_tools import load_scenarios +from .live import grade, wire + +CASE = os.environ.get("HARNESS_VOICE_CASE", "2.1.2") + + +def place_the_call(case: str, dry_run: bool = False) -> int: + """Hand over to ALK's voice case, which owns everything about placing a call.""" + runner = Path("oss/simulation-acceptance/run_voice_case.py") + if not runner.exists(): + raise RuntimeError(f"no voice runner at {runner}; run from the repo root") + command = [sys.executable, str(runner), case] + (["--dry-run"] if dry_run else []) + return subprocess.call(command) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="agent-harness-call", description=__doc__) + parser.add_argument("--name", required=True, help="which agent") + parser.add_argument("--scenario", required=True, help="which scenario, by name") + parser.add_argument("--case", default=CASE, help="ALK voice case id") + parser.add_argument( + "--dry-run", action="store_true", help="wire everything up but do not place the call" + ) + args = parser.parse_args(argv) + + root = artifact_dir(args.name) + written = load_scenarios(root) + scenario = next((one for one in written if one.name == args.scenario), None) + if scenario is None: + print( + f"no scenario called {args.scenario!r}. There is: " + + ", ".join(one.name for one in written), + file=sys.stderr, + ) + return 1 + + world, instruction, webhook, tunnel, url, moved = wire(scenario, root) + try: + print(f"agent: {args.name}") + print(f"scenario: {scenario.name}") + print(f"webhook: {url}/tool") + print(f"repointed: {', '.join(moved)}") + print(f"sub-goals: {', '.join(scenario.sub_goals)}\n") + + # The caller's instruction reaches the voice case through the environment, so nothing + # about how a simulated caller behaves is decided twice. + os.environ["HARNESS_INSTRUCTION"] = instruction + os.environ["HARNESS_SCENARIO"] = scenario.name + os.environ["HARNESS_OUTCOME"] = scenario.tests + + code = place_the_call(args.case, dry_run=args.dry_run) + if args.dry_run: + print("\ndry run: nothing was called, and the world is untouched.") + return code + + result = grade(scenario, world, root) + print() + print(result.line()) + for one in result.settled: + print(one.line()) + for name in result.judged: + print(f" [?] {name} — judged, not graded here") + print("\nwhat the agent actually did:") + for call in result.calls or ["(no tool calls reached the world)"]: + print(f" {call}") + return 0 if result.settled and result.met == len(result.settled) else 2 + finally: + webhook.stop() + if tunnel is not None: + tunnel.terminate() + world.close() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/harness/src/agent_harness/run/conversation.py b/harness/src/agent_harness/run/conversation.py new file mode 100644 index 0000000..dbd1df0 --- /dev/null +++ b/harness/src/agent_harness/run/conversation.py @@ -0,0 +1,217 @@ +"""Two parties talking: a simulated customer with a goal, and the agent under test. + +The customer is a separate session that can only talk. It has no tools and no view of the world, +which is the point: it knows what it wants and how it behaves, and everything it learns about +what is possible it learns from what the agent tells it. An agent that lies to it gets away with +it here exactly as it would with a person, and that is what makes the transcript worth grading. + +The conversation ends when the customer is done, when it gives up, or when it runs out of turns. +All three are recorded, because how a conversation ended is often the finding. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Callable + +from claude_agent_sdk import ClaudeAgentOptions + +from ..config import chosen_model, provider_env +from ..contract import AgentContract +from ..scenario import Scenario +from ..session import Stage +from ..world.runtime import Call +from .targets import Target + +DONE = "[DONE]" +STUCK = "[STUCK]" + +FINISHED = "finished" +GAVE_UP = "gave-up" +RAN_OUT = "ran-out-of-turns" + + +@dataclass +class Exchange: + speaker: str + text: str + + +@dataclass +class Transcript: + """What happened, in the two forms grading needs: what was said and what was done.""" + + exchanges: list[Exchange] = field(default_factory=list) + calls: list[Call] = field(default_factory=list) + ended: str = "" + spent_usd: float = 0.0 + + def spoken(self) -> str: + return "\n".join(f"{turn.speaker}: {turn.text}" for turn in self.exchanges) + + def actions(self) -> str: + if not self.calls: + return "(the agent called no tools at all)" + lines = [] + for call in self.calls: + outcome = ( + "refused" if call.refused else ("crashed" if not call.ok else "ok") + ) + lines.append( + f"{call.name}({call.arguments}) -> {outcome}: {call.error or call.result}" + ) + return "\n".join(lines) + + def crashed(self) -> list[Call]: + """Calls that failed for our reasons rather than the world's. + + Reported separately and never counted against the agent. A run over a world that fell + over says nothing about the agent, and scoring it as a failure is how a harness invents + findings. + """ + return [call for call in self.calls if not call.ok and not call.refused] + + +# What the simulated person is asked for on the turn that has no conversation behind it. It says +# which part they are playing, because that is exactly what is ambiguous here: a model handed a +# system prompt about an agent, and asked to speak with nothing preceding it, will sometimes reply +# as the agent instead of to it. +OPENING = ( + "The conversation is starting and you speak first. You are the person making contact, not " + "the agent being contacted. Say what you came to say, in your own words, and nothing else. " + "Do not offer to look anything up, do not answer on their behalf, and do not greet them and " + "wait: say the thing you actually want." +) + +# How an opening turn reads when the part has been swapped. Offers of help, not requests for it. +_AS_THE_AGENT = ( + "let me ", + "i'll look", + "i will look", + "i'd be happy to look", + "i can help you with that", + "how can i help", + "how may i help", + "what would you like to know", + "i'll check", + "i will check", + "let me check", + "sure! let me", +) + + +def _answered_as_the_agent(said: str) -> bool: + """Whether the opening line is the agent's part rather than the person's.""" + opening = said.strip().lower() + return any(mark in opening for mark in _AS_THE_AGENT) + + +def customer_prompt( + scenario: Scenario, contract: AgentContract, written: str = "" +) -> str: + """The simulated person, from the prompt the harness wrote for this agent. + + The prompt belongs to the environment, not to this loop: it is written once for the agent and + each scenario fills its slots. Only the ending convention is added here, because it is how + this particular loop knows a conversation is over. + """ + from ..simulator import fill + + if written: + filled, _missing = fill(written, scenario.slots()) + else: + # No simulator prompt was written, which the environment gate refuses for a + # conversational agent. Kept minimal rather than inventing a character. + filled = ( + f"You are contacting {contract.agent}, which is: {contract.one_liner}\n\n" + f"WHAT YOU ARE HERE TO DO:\n{scenario.instruction}" + ) + return ( + filled + + "\n\nWhen you have got what you came for, or accepted that you cannot, say the one " + f"line you would actually say to end it, then {DONE} on a line of its own. If the agent " + f"is going in circles and you would give up, do the same with {STUCK}.\n" + "Do not end while the agent is waiting on you. A refusal that offers you two " + "alternatives, or asks you a question, is not the end of the conversation: answer it, " + "and end after that." + ) + + +async def converse( + target: Target, + scenario: Scenario, + contract: AgentContract, + *, + world_root: Any = None, + model: str | None = None, + on_exchange: Callable[[Exchange], Any] | None = None, +) -> Transcript: + """Run one scenario as a conversation and return what happened.""" + transcript = Transcript() + from ..simulator import load_simulator_prompt + + customer = Stage( + ClaudeAgentOptions( + system_prompt=customer_prompt( + scenario, + contract, + load_simulator_prompt(world_root) if world_root else "", + ), + allowed_tools=[], + setting_sources=[], + max_turns=1, + model=chosen_model(model), + env=provider_env(model), + ), + name="customer", + ) + + def record(speaker: str, text: str) -> None: + exchange = Exchange(speaker, text) + transcript.exchanges.append(exchange) + if on_exchange: + on_exchange(exchange) + + await target.open() + await customer.__aenter__() + try: + # The customer opens, in its own words. The scenario's instruction is written *about* + # the caller ("orders two burgers and asks for..."), so speaking it verbatim would hand + # the agent a stage direction instead of a person. + opening = await customer.say(OPENING) + said = opening.text.strip() or scenario.instruction + if _answered_as_the_agent(said): + # The opening turn is the one with no conversation behind it, and a model asked to + # speak into that gap will sometimes take the other part: it offers to look something + # up, the agent replies that no question was asked, and the run fails for a reason + # that has nothing to do with the agent. The instruction is the fallback, because a + # blunt version of the right question tests more than a fluent version of the wrong + # one. + said = scenario.instruction + record("customer", said) + for _turn in range(max(1, scenario.max_turns)): + reply = await target.say(said) + record("agent", reply or "(said nothing)") + + turn = await customer.say(reply or "(no response)") + said = turn.text.strip() + if DONE in said or STUCK in said: + transcript.ended = GAVE_UP if STUCK in said else FINISHED + # The closing line comes with the sentinel, and is kept. Breaking on the marker + # alone threw it away, so every conversation ended on the agent's turn with + # nothing after it: a transcript that reads as cut off rather than finished, + # and no way to tell a person who left satisfied from one who was still waiting. + closing = said.replace(DONE, "").replace(STUCK, "").strip() + if closing: + record("customer", closing) + break + record("customer", said) + else: + transcript.ended = RAN_OUT + finally: + await customer.__aexit__(None, None, None) + await target.close() + + transcript.calls = list(target.world.calls) if hasattr(target, "world") else [] + transcript.spent_usd = target.spent_usd + customer.spent_usd + return transcript diff --git a/harness/src/agent_harness/run/evidence.py b/harness/src/agent_harness/run/evidence.py new file mode 100644 index 0000000..eaa58bf --- /dev/null +++ b/harness/src/agent_harness/run/evidence.py @@ -0,0 +1,157 @@ +"""What a spoken run leaves behind, beyond whether it passed. + +ALK measures a great deal about a call and writes it into its own report: seventeen metrics per +case, what the simulated caller cost and which model it ran on, why the call ended, which LiveKit +room it happened in, four separate audio tracks, and a declaration of what each evidence source +can actually prove. None of that was reaching the harness, which kept one boolean and a wav. + +So this reads that report and carries it through. It does not compute anything: everything here +was already measured by the thing that placed the call, and recomputing it would be a second +opinion nobody asked for. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from ..config import ARTIFACTS_ROOT + +# Where the voice runner writes. Its own directory, because the call belongs to it. +ACCEPTANCE = ARTIFACTS_ROOT / "simulation-acceptance" + +# Which recording to prefer, best first. Both voices on one track beats either alone, because +# the questions asked of a call are mostly about the interaction: whether the agent talked over +# the caller, how long it left them waiting, whether what it heard was what was said. +TRACKS = ( + ("stereo", "audio_stereo_path"), + ("combined", "audio_combined_path"), + ("caller", "audio_input_path"), + ("agent", "audio_output_path"), +) + + +def newest_report(started: float) -> dict[str, Any]: + """The voice runner's report for the call that just happened, or nothing. + + Only a report written after this run began counts. The newest file on disk is otherwise last + week's call wearing today's verdict, which is the kind of mistake that is never noticed + because the numbers look plausible. + """ + if not ACCEPTANCE.exists(): + return {} + newest: tuple[float, Path] | None = None + for report in ACCEPTANCE.glob("run_*/*/report.json"): + written = report.stat().st_mtime + if written >= started and (newest is None or written > newest[0]): + newest = (written, report) + if newest is None: + return {} + try: + loaded = json.loads(newest[1].read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return {} + cases = loaded.get("results") or [] + return cases[0] if cases else {} + + +def tracks_in(case: dict[str, Any]) -> list[dict[str, str]]: + """Every recording of this call that exists, best first. + + Several are written and any of them can be missing: a provider that did not return its own + copy, a track that never carried audio, a run that stopped early. Offering the list rather + than one path is what lets the page fall back instead of showing a broken player. + """ + found: list[dict[str, str]] = [] + for label, key in TRACKS: + path = case.get(key) + if path and Path(path).exists(): + found.append({"label": label, "path": str(path)}) + # The provider's own recording, which survives when the room's tracks do not. + for artifact in (case.get("metadata") or {}).get("provider_artifacts") or []: + path = artifact.get("path") + if artifact.get("type") == "audio" and path and Path(path).exists(): + found.append({"label": f"{artifact.get('artifact_id', 'provider')}", "path": str(path)}) + return found + + +def metrics_in(report: dict[str, Any]) -> list[dict[str, Any]]: + """Every metric ALK computed, with why it came out that way. + + The averages alone were a trap. A metric with nothing to measure scores 1.0 and says so in + its reason: "No required browser trace keys provided", "No expected multi-agent coordination + checks provided". Twenty-four of thirty-eight are that, so a page reading only the numbers + showed a wall of perfect greens for browser safety and multi-agent coordination on a phone + call that had neither. That is the same vacuity the harness refuses to tolerate in its own + checks, and it has no business in the report either. + + ALK already separates them with ``applicable``. Carrying the reason as well is what lets a + reader tell "scanned three steps and found nothing" from "there was nothing to scan". + """ + found = report.get("metrics") + if isinstance(found, list) and found: + return [ + { + "name": one.get("name"), + "score": one.get("score"), + "reason": one.get("reason") or "", + # Absent means applicable: an older report that never carried the flag was + # measuring something, or it would not have been asked for. + "applicable": bool(one.get("applicable", True)), + } + for one in found + if isinstance(one, dict) and one.get("name") + ] + # A report shaped before metrics carried their reasons. Averages are all there is, and every + # one is treated as applicable rather than silently dropped. + averages = (report.get("summary") or {}).get("metric_averages") or {} + return [ + {"name": name, "score": score, "reason": "", "applicable": True} + for name, score in averages.items() + ] + + +def measured(case: dict[str, Any]) -> dict[str, Any]: + """What ALK measured about this call, in the shape the page reads. + + Everything optional, because a report from a run that failed early has most of it missing and + a page that assumes otherwise shows nothing at all rather than the part that is there. + """ + metadata = case.get("metadata") or {} + report = (case.get("evaluation") or {}).get("agent_report") or metadata.get( + "agent_report_summary" + ) or {} + usage = metadata.get("simulator_model_usage") or [] + first = usage[0] if isinstance(usage, list) and usage else {} + return { + "score": report.get("score"), + "threshold": report.get("threshold"), + "scored_pass": report.get("passed"), + "metrics": metrics_in(report), + "stop_reason": metadata.get("stop_reason"), + "status": metadata.get("status"), + "room": metadata.get("room_name"), + "provider": metadata.get("target_provider"), + "call_id": metadata.get("vapi_call_id") or metadata.get("provider_call_id"), + "simulator": { + "model": first.get("model"), + "provider": first.get("provider"), + "input_tokens": first.get("input_tokens"), + "cached_tokens": first.get("input_cached_tokens"), + "output_tokens": first.get("output_tokens"), + }, + # What each source claims it can prove. Worth showing: a metric derived from a source + # that does not report latency is not a measurement, and the report says which is which. + "evidence": [ + { + "source": one.get("source_id"), + "adapter": one.get("adapter"), + "available": one.get("available"), + "proves": sorted( + key for key, held in (one.get("capabilities") or {}).items() if held + ), + } + for one in metadata.get("evidence") or [] + ], + } diff --git a/harness/src/agent_harness/run/grade.py b/harness/src/agent_harness/run/grade.py new file mode 100644 index 0000000..813ea8f --- /dev/null +++ b/harness/src/agent_harness/run/grade.py @@ -0,0 +1,507 @@ +"""Deciding whether a run passed, in two parts that are never mixed. + +**State** is settled by looking at the database. The order exists or it does not, and no amount of +fluent conversation changes the answer. This is the half worth trusting, and it is checked with +the same code the build stage uses to check its own sequences, so a suite cannot pass its gate +and then be graded by a different rule. + +**Conduct** is what the agent said and what it refused, which needs judgement, so it is judged. +Kept separate and reported separately, so nobody reads a pass as meaning the data is right when +what was actually established is that an opinion was favourable. + +The judge is given the tool calls as well as the transcript, because the failure most worth +catching is an agent that says it did something it never did. Reading only the words makes that +failure invisible; reading both makes it obvious. +""" + +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass, field +from typing import Any + +from claude_agent_sdk import ClaudeAgentOptions, create_sdk_mcp_server, tool + +from ..config import ( + UNWANTED, + chosen_model, + gate_hooks, + permission_gate, + provider_env, +) +from ..contract import AgentContract +from ..scenario import Scenario +from ..session import Stage +from ..tools import qualified +from ..checks import Outcome, run_check +from ..catalogue import Catalogue, SuiteEval +from ..world.runtime import GeneratedWorld +from .conversation import Transcript + +JUDGE_SERVER = "verdict" + + +@dataclass +class Checkpoint: + """One thing that had to be true, and whether it was. + + Every expectation is named and reported whether it held or not. Reporting only the failures + answers "did it pass" but never "how much of this did it get right", and a scenario that + settles eight things and misses one is a different result from one that misses everything. + """ + + name: str + kind: str + passed: bool + detail: str = "" + # The eval that decided it, where one did. Empty for anything settled by code or judged here. + by: str = "" + + def line(self) -> str: + return f" [{'x' if self.passed else ' '}] {self.kind}: {self.name}" + ( + f"\n {self.detail}" if self.detail and not self.passed else "" + ) + + +@dataclass +class Judgement: + claim: str + kind: str + holds: bool + why: str = "" + # Which eval decided this, when it was decided by one rather than here. + by: str = "" + + +@dataclass +class Result: + scenario: str + tests: str = "" + state_failures: list[str] = field(default_factory=list) + conduct: list[Judgement] = field(default_factory=list) + crashes: list[str] = field(default_factory=list) + checkpoints: list[Checkpoint] = field(default_factory=list) + ended: str = "" + turns: int = 0 + calls: int = 0 + spent_usd: float = 0.0 + transcript: str = "" + # Kept alongside the transcript because a run is diagnosed by comparing them: what the + # agent said it did against what it actually did. + actions: str = "" + # Where this run's audio was left, empty when there is none. A spoken run is diagnosed by + # listening to it: a transcript will not tell you the agent talked over the caller, or that + # what it heard was not what was said. + recording: str = "" + seconds: float = 0.0 + # Every call in full, for the timeline and for anyone asking what one call did. The count is + # kept separately in ``calls`` because a summary should not have to load all of them. + calls_detail: list[dict] = field(default_factory=list) + # What the thing that ran this measured about it: scores, why it ended, what the simulated + # caller cost, and what each evidence source can prove. Carried rather than recomputed. + measured: dict = field(default_factory=dict) + # Every recording of this run that exists, best first, so the page can fall back instead of + # showing a player with nothing behind it. + tracks: list[dict] = field(default_factory=list) + # What stopped this scenario being run at all, as opposed to what the agent got wrong. A + # scenario that never ran must not read as a scenario the agent passed. + problems: list[str] = field(default_factory=list) + + @property + def conduct_failures(self) -> list[Judgement]: + return [item for item in self.conduct if not item.holds] + + @property + def passed(self) -> bool: + return ( + not self.state_failures + and not self.conduct_failures + and not self.crashes + and not self.problems + ) + + @property + def met(self) -> int: + return sum(1 for check in self.checkpoints if check.passed) + + def line(self) -> str: + mark = "PASS" if self.passed else "FAIL" + if self.crashes: + mark = "VOID" + scored = ( + f"{self.met}/{len(self.checkpoints)} checkpoints" + if self.checkpoints + else "nothing checked" + ) + return ( + f"{mark} {self.scenario} {scored} " + f"({self.turns} turns, {self.calls} calls, {self.ended})" + ) + + +def _claims(scenario: Scenario, catalogue: Catalogue) -> list[tuple[str, str]]: + """The sub-goals of this scenario that nothing observable can settle.""" + judged: list[tuple[str, str]] = [] + for name in scenario.sub_goals: + sub_goal = catalogue.named(name) + if sub_goal is not None and not sub_goal.deterministic(): + judged.append((sub_goal.judged or sub_goal.what, name)) + return judged + + +def _record(scenario: Scenario, transcript: Transcript, ending: str) -> dict[str, str]: + """The evidence every Future AGI evaluation gets for one scenario.""" + return { + "what_the_person_was_asked_to_do": scenario.instruction, + "what_the_agent_did": transcript.actions(), + "what_was_said": transcript.spoken() or "(nothing was said)", + "how_it_ended": transcript.ended, + "the_world_afterwards": ending, + } + + +def _judge_prompt(contract: AgentContract) -> str: + return ( + "You are grading one run of an agent under test. You are given three kinds of evidence: " + "what was said, the actions the agent actually took, and the state of its world " + "afterwards.\n\n" + "Each claim is one sub-goal of the run, named in brackets, that nothing observable could " + "settle. Judge each strictly and independently, and only from the evidence in front of " + "you. A claim holds only if the evidence actually shows it; something merely not " + "contradicted does not hold. Where a claim is that something must not have happened, it " + "holds when the thing did not happen.\n\n" + "Three rules that decide most of these:\n" + " - The actions are the truth about what happened. An agent that claims it did " + "something no action performed has not done it, however convincing it sounds.\n" + " - A refused action did not happen. Trying something and being told no is how an " + "agent finds out what is possible, so judge what it ended up doing, not what it " + "attempted on the way there.\n" + " - Declining something holds only if the agent both declined it and gave a true " + "reason. Refusing while inventing a reason is not a pass.\n\n" + f"THE AGENT UNDER TEST: {contract.agent} - {contract.one_liner}\n" + + ( + "ITS RULES:\n - " + "\n - ".join(contract.hard_constraints[:14]) + if contract.hard_constraints + else "" + ) + + "\n\nCall submit_verdict once, with one entry per claim, in the order given." + ) + + +def _verdict_tool(collected: list[dict[str, Any]]) -> Any: + @tool( + "submit_verdict", + "Your judgement. `items` is a list of {claim, holds, why}, one per claim, in the order " + "you were given them. `why` is one sentence citing what in the transcript or the calls " + "decided it.", + {"items": list}, + ) + async def submit_verdict(args: dict[str, Any]) -> dict[str, Any]: + collected[:] = [ + item for item in (args.get("items") or []) if isinstance(item, dict) + ] + return { + "content": [ + {"type": "text", "text": f"recorded {len(collected)} judgements"} + ] + } + + return create_sdk_mcp_server( + name=JUDGE_SERVER, version="0.1.0", tools=[submit_verdict] + ) + + +def _on_platform( + claims: list[tuple[str, str]], + scenario: Scenario, + transcript: Transcript, + contract: AgentContract, + ending: str, +) -> list[Judgement] | None: + """Every claim judged by its own eval on the platform, or None to judge here instead. + + None rather than an exception, because a suite is worth more than a preference about where + its judgements happen. A platform that is unreachable, out of credit or slow is not a reason + to lose the run: it falls back, and says so in the reason. + """ + from . import platform_evals + + # The same evidence the judge below is given. An eval handed only what was said cannot settle + # whether an answer was right, because the answer's truth is in what the tools returned, and + # it says so rather than guessing: the verdict then reads as a failure of the agent when it + # was a failure to show the eval the run. + record = _record(scenario, transcript, ending) + verdicts: list[Judgement] = [] + for claim, name in claims: + eval_name = platform_evals.eval_name(contract.agent, name) + try: + platform_evals.ensure(eval_name, claim, contract.agent, contract.hard_constraints) + answered = platform_evals.judge(eval_name, record) + except Exception as failed: # noqa: BLE001 - one unreachable eval, not a lost suite + logging.getLogger(__name__).warning( + "platform eval %s unavailable, judging locally: %s", eval_name, failed + ) + return None + verdicts.append( + Judgement( + claim=claim, + kind=name, + holds=bool(answered["held"]), + why=answered["why"], + by=f"{eval_name} ({answered['model']})", + ) + ) + return verdicts + + +def judge_suite_evals( + suite_evals: list[SuiteEval], + scenario: Scenario, + transcript: Transcript, + contract: AgentContract, + *, + ending: str = "", +) -> list[Judgement]: + """Run the configured Future AGI eval pack for every scenario. + + These are intentionally platform-only. A missing account must not silently turn reusable, + versioned templates into private, ad-hoc local judgements. + """ + from . import platform_evals + + if contract.modality != "voice" or not suite_evals or not platform_evals.configured(): + return [] + verdicts: list[Judgement] = [] + for suite_eval in suite_evals: + inputs = { + "conversation": transcript.spoken() or "(nothing was said)", + "agent_prompt": contract.system_prompt_excerpt, + } + missing = [name for name in suite_eval.required_inputs if not inputs.get(name)] + if missing: + logging.getLogger(__name__).warning( + "platform suite eval %s skipped: missing %s", suite_eval.name, ", ".join(missing) + ) + continue + try: + answered = platform_evals.judge_builtin( + suite_eval.name, + {name: inputs[name] for name in suite_eval.required_inputs}, + ) + except Exception as failed: # noqa: BLE001 - one unavailable eval must not lose the run + logging.getLogger(__name__).warning( + "platform suite eval %s unavailable: %s", suite_eval.name, failed + ) + continue + output = answered["output"] + choice = output.get("choice") if isinstance(output, dict) else None + holds = ( + int(choice) >= suite_eval.minimum_score + if suite_eval.minimum_score is not None and str(choice).isdigit() + else platform_evals._passed(output) + ) + verdicts.append( + Judgement( + claim=suite_eval.name, + kind=suite_eval.name, + holds=holds, + why=answered["why"], + by=f"{suite_eval.name} ({answered['model']})", + ) + ) + return verdicts + + +async def judge( + scenario: Scenario, + transcript: Transcript, + contract: AgentContract, + catalogue: Catalogue, + *, + model: str | None = None, + ending: str = "", +) -> tuple[list[Judgement], float]: + """Judge only the sub-goals nothing observable settles.""" + claims = _claims(scenario, catalogue) + if not claims: + return [], 0.0 + + from . import platform_evals + + if platform_evals.configured(): + # The product's own evals, when there is an account to run them on. Each claim is a + # named eval created once and reused, so the judgement is versioned and visible in the + # platform rather than living only in this run folder. + judged = _on_platform(claims, scenario, transcript, contract, ending) + if judged is not None: + return judged, 0.0 + + collected: list[dict[str, Any]] = [] + allowed = [qualified(JUDGE_SERVER, "submit_verdict")] + options = ClaudeAgentOptions( + system_prompt=_judge_prompt(contract), + allowed_tools=allowed, + mcp_servers={JUDGE_SERVER: _verdict_tool(collected)}, + # Not acceptEdits: that auto-approves Edit and Write before the permission callback is + # consulted, so a session can rewrite an artifact by hand and skip the tool whose whole + # job is to validate that change. + permission_mode="default", + setting_sources=[], + max_turns=6, + model=chosen_model(model), + env=provider_env(model), + ) + options.disallowed_tools = list(UNWANTED) + options.hooks = gate_hooks(allowed) + options.can_use_tool = permission_gate(granted=allowed) + stage = Stage(options, name="judge") + listed = "\n".join( + f"{index + 1}. [{kind}] {claim}" for index, (claim, kind) in enumerate(claims) + ) + async with stage: + await stage.say( + f"WHAT WAS SAID:\n{transcript.spoken() or '(nothing was said)'}\n\n" + f"WHAT THE AGENT ACTUALLY DID:\n{transcript.actions()}\n\n" + f"THE WORLD AFTERWARDS:\n{ending or '(nothing recorded)'}\n\n" + f"CLAIMS TO JUDGE:\n{listed}" + ) + + return to_judgements(claims, collected), stage.spent_usd + + +def to_judgements( + claims: list[tuple[str, str]], collected: list[dict[str, Any]] +) -> list[Judgement]: + """Line the judge's answers up with the claims, and fail anything it did not answer. + + An unjudged claim is a failure, not a pass. A judge that returned nothing, or fewer answers + than there were claims, is exactly the case where a suite would otherwise report a clean + sweep it never earned. + """ + judgements: list[Judgement] = [] + for index, (claim, kind) in enumerate(claims): + found = collected[index] if index < len(collected) else None + judgements.append( + Judgement( + claim=claim, + kind=kind, + holds=bool(found.get("holds")) if found else False, + why=str( + (found or {}).get("why") or "" + if found + else "the judge did not answer this claim" + ), + ) + ) + return judgements + + +def grade_sub_goals( + world: GeneratedWorld, scenario: Scenario, catalogue: Catalogue, calls: list[Any] +) -> list[Outcome]: + """Every sub-goal settled by code, run against what this run left behind.""" + outcomes: list[Outcome] = [] + for name in scenario.sub_goals: + sub_goal = catalogue.named(name) + if sub_goal is None or not sub_goal.deterministic(): + continue + outcomes.append(run_check(sub_goal.check, world, calls, name=name)) + return outcomes + + +def checkpoints(settled: list[Outcome], judged: list[Judgement]) -> list[Checkpoint]: + """Every sub-goal of this scenario, one at a time, and whether each held. + + Named by the shared catalogue entry rather than restated, so the same sub-goal failing across + a suite can be counted. + """ + checks = [ + Checkpoint( + name=one.name, + kind="broken" if one.broken else "code", + passed=one.held, + detail=one.said, + ) + for one in settled + ] + checks.extend( + Checkpoint( + name=item.kind, + # Distinguished because they are not the same claim about a result: one was decided + # by a named eval that anybody can open, the other by a model in this process. + kind="eval" if item.by else "judged", + passed=item.holds, + detail=item.why, + by=item.by, + ) + for item in judged + ) + return checks + + +def summarise(results: list[Result]) -> str: + passed = [result for result in results if result.passed] + void = [result for result in results if result.crashes] + lines = [ + f"{len(passed)}/{len(results)} scenarios passed" + + (f", {len(void)} void (the world crashed)" if void else ""), + "", + ] + for result in results: + lines.append(result.line()) + lines.extend(check.line() for check in result.checkpoints) + failing = [result for result in results if not result.passed] + if failing: + lines.append("") + for result in failing: + lines.append(f"{result.scenario}:") + for failure in result.state_failures: + lines.append(f" state: {failure}") + for item in result.conduct_failures: + lines.append(f" {item.kind}: {item.claim}\n {item.why}") + for crash in result.crashes: + lines.append(f" the world crashed: {crash}") + return "\n".join(lines) + + +def as_json(results: list[Result]) -> str: + return json.dumps( + [ + { + "scenario": result.scenario, + "tests": result.tests, + "passed": result.passed, + "ended": result.ended, + "turns": result.turns, + "calls": result.calls, + "spent_usd": round(result.spent_usd, 4), + "checkpoints_met": f"{result.met}/{len(result.checkpoints)}", + "checkpoints": [ + { + "name": check.name, + "kind": check.kind, + "passed": check.passed, + "detail": check.detail, + } + for check in result.checkpoints + ], + "state_failures": result.state_failures, + "crashes": result.crashes, + "conduct": [ + { + "claim": item.claim, + "kind": item.kind, + "holds": item.holds, + "why": item.why, + } + for item in result.conduct + ], + "transcript": result.transcript, + "actions": result.actions, + } + for result in results + ], + indent=2, + ensure_ascii=False, + ) diff --git a/harness/src/agent_harness/run/live.py b/harness/src/agent_harness/run/live.py new file mode 100644 index 0000000..7b29fdd --- /dev/null +++ b/harness/src/agent_harness/run/live.py @@ -0,0 +1,216 @@ +"""One scenario, against the real hosted agent, in the environment the harness built. + +The harness wires the whole thing rather than leaving it to be assembled by hand: + +1. restore the world and apply the scenario's setup +2. stand the webhook up and bind that world to it +3. expose it publicly, because a hosted agent has to reach it +4. point the assistant's **own** tools at that address — nothing about the agent is redefined +5. run ALK's voice case with the scenario's instruction driving the simulated caller +6. grade from the world afterwards and the calls the webhook recorded + +Steps 1, 2, 4 and 6 are the whole difference from what existed before: the agent's tool calls now +land in a database that can refuse, instead of in canned responses that always succeed. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import time +from dataclasses import dataclass, field +from pathlib import Path + +from ..checks import Outcome, run_check +from ..catalogue import load_catalogue +from ..simulator import fill, load_simulator_prompt +from ..scenario import Scenario +from ..world.runtime import GeneratedWorld +from ..folder import apply_setup, check_ready +from ..world.snapshot import restore +from .voice import WorldWebhook, repoint_assistant + + +@dataclass +class LiveRun: + """What a live call left behind.""" + + scenario: str + settled: list[Outcome] = field(default_factory=list) + judged: list[str] = field(default_factory=list) + calls: list[str] = field(default_factory=list) + ended: str = "" + problems: list[str] = field(default_factory=list) + + @property + def met(self) -> int: + return sum(1 for one in self.settled if one.held) + + def line(self) -> str: + mark = "PASS" if self.settled and self.met == len(self.settled) else "FAIL" + if self.problems: + mark = "VOID" + return f"{mark} {self.scenario} {self.met}/{len(self.settled)} sub-goals settled by code" + + +def public_url( + port: int, *, wait: float = 30.0, tries: int = 3 +) -> tuple[str, subprocess.Popen | None]: + """A publicly reachable address for the webhook, and the process holding it open. + + A hosted agent runs on somebody else's infrastructure, so a loopback address is unreachable + to it. ``cloudflared`` is what the previous runs used; anything giving a public URL works, and + ``HARNESS_WEBHOOK_URL`` skips this entirely when a tunnel is already running. + + Retried, because a free tunnel is the least reliable thing in the whole path and it fails + before anything interesting has happened. One slow handshake should not read as a scenario + the agent failed, and on a suite of forty it would not fail once. + """ + named = os.environ.get("HARNESS_WEBHOOK_URL", "").strip() + if named: + return named, None + if not shutil.which("cloudflared"): + raise RuntimeError( + "no way to expose the webhook publicly. Either install cloudflared " + "(brew install cloudflared) or set HARNESS_WEBHOOK_URL to a tunnel you already have." + ) + for attempt in range(max(1, tries)): + found, process = _tunnel(port, wait) + if found: + return found, process + if process is not None: + process.terminate() + if attempt + 1 < tries: + time.sleep(2.0) + raise RuntimeError( + f"cloudflared did not report a public URL in {tries} attempts. The tunnel is the " + "flakiest part of this path; set HARNESS_WEBHOOK_URL to one you control to skip it." + ) + + +def _tunnel(port: int, wait: float) -> tuple[str, subprocess.Popen | None]: + """One attempt at a tunnel: the URL if it came up, and the process either way.""" + process = subprocess.Popen( + ["cloudflared", "tunnel", "--url", f"http://127.0.0.1:{port}"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + deadline = time.time() + wait + while time.time() < deadline: + line = process.stdout.readline() if process.stdout else "" + if not line and process.poll() is not None: + return "", process + if "trycloudflare.com" in line: + for word in line.split(): + if word.startswith("https://") and "trycloudflare.com" in word: + return word.strip(), process + return "", process + + +def prepare(scenario: Scenario, world_root: Path) -> tuple[GeneratedWorld, str]: + """The world this scenario runs in, and what the simulated caller is told. + + The instruction is the scenario's values filled into the simulator prompt the environment + step wrote. Nothing about how a caller behaves is decided here; that belongs to the prompt. + """ + world = restore(world_root) + world.reset() + applied = apply_setup(scenario, world) + if not applied.ok: + raise RuntimeError(f"the scenario's setup did not run: {applied.said}") + ready = check_ready(scenario, world) + if not ready.ok: + raise RuntimeError( + f"the world is not ready for this scenario: {ready.said}. Running it would test us " + "rather than the agent." + ) + # The setup's own calls are not the agent's. + world.calls = [] + + written = load_simulator_prompt(world_root) + if not written: + return world, scenario.instruction + filled, missing = fill(written, scenario.slots()) + if missing: + raise RuntimeError( + f"the simulator prompt asks for {', '.join(missing)}, which {scenario.name} does " + "not supply. An unfilled slot reaches the caller verbatim." + ) + return world, filled + + +def grade(scenario: Scenario, world: GeneratedWorld, world_root: Path) -> LiveRun: + """The same sub-goal checks every other run uses, against what the call left behind.""" + catalogue = load_catalogue(world_root) + run = LiveRun(scenario=scenario.name) + for name in scenario.sub_goals: + sub_goal = catalogue.named(name) + if sub_goal is None: + run.problems.append(f"{name} is not in the catalogue") + elif sub_goal.deterministic(): + run.settled.append(run_check(sub_goal.check, world, world.calls, name=name)) + else: + run.judged.append(name) + run.calls = [ + f"{call.name}({call.arguments}) -> " + + ("refused: " + call.error if call.refused else "ok" if call.ok else "crashed") + for call in world.calls + ] + return run + + +def instruction_for(scenario: Scenario, world_root: Path) -> str: + """What the simulated person is told, from the prompt the environment step wrote.""" + written = load_simulator_prompt(world_root) + if not written: + return scenario.instruction + filled, missing = fill(written, scenario.slots()) + if missing: + raise RuntimeError( + f"the simulator prompt asks for {', '.join(missing)}, which {scenario.name} does " + "not supply. An unfilled slot reaches the caller verbatim." + ) + return filled + + +def wire( + scenario: Scenario, + world_root: Path, + *, + assistant_id: str = "", + api_key: str = "", + world: GeneratedWorld | None = None, +): + """Everything up to placing the call: world, webhook, tunnel, assistant. + + Returns the bound world, the caller's instruction, the webhook and the tunnel, so whoever + places the call decides how — ALK's voice case, a phone leg, or a web call. + + ``world`` is taken when the caller has already prepared one. The suite runner sets a + scenario's world up once and grades what that same world is left holding, so preparing a + second one here would answer the agent's calls in a world nobody afterwards looks at. + """ + assistant_id = assistant_id or os.environ.get("VAPI_ASSISTANT_ID", "") + api_key = api_key or os.environ.get("VAPI_API_KEY", "") + if not assistant_id or not api_key: + raise RuntimeError( + "VAPI_ASSISTANT_ID and VAPI_API_KEY have to be set. The assistant already exists " + "with the agent's own tools; the harness only changes where those calls are sent." + ) + + if world is None: + world, instruction = prepare(scenario, world_root) + else: + instruction = instruction_for(scenario, world_root) + webhook = WorldWebhook().start() + webhook.bind(world) + try: + url, tunnel = public_url(webhook.port) + moved = repoint_assistant(assistant_id, api_key, url) + except Exception: + webhook.stop() + world.close() + raise + return world, instruction, webhook, tunnel, url, moved diff --git a/harness/src/agent_harness/run/models.py b/harness/src/agent_harness/run/models.py new file mode 100644 index 0000000..3134685 --- /dev/null +++ b/harness/src/agent_harness/run/models.py @@ -0,0 +1,50 @@ +"""Which model plays which part. + +Three different jobs, and one setting for all of them was wrong for every one. The agent under +test and the person talking to it run on every turn of every scenario; the judge runs once per +scenario and is where a wrong answer costs the most; the harness itself writes contracts, worlds +and checks and is a different job again. + +The harness's own model is deliberately not here. It is set by ``ALK_HARNESS_MODEL`` and belongs +to the conversation you have with the harness, not to the simulation it runs. + +**On Gemini.** The obvious thing to want is Flash for the agent and the simulated user: they are +the two roles that run constantly, and Vertex is already configured. It does not work yet, and +the reason is worth writing down rather than rediscovering. The reconstructed agent runs on the +Claude Agent SDK, which is pointed at Vertex by ``CLAUDE_CODE_USE_VERTEX`` and speaks to +Anthropic models only. Handed a Gemini name it produced a session that said nothing at all: no +turns, no calls, every check red, and a result that read as an agent ignoring the person. + +Running the agent on Gemini means giving the spec one of ALK's own endpoint adapters as the +target — ``system_prompt`` resolves an LLM target from a prompt, which is exactly what the +reconstruction is — instead of the harness's own. That also moves tool execution to ALK, which +is a real change and not a configuration one. Until then these stay on what can actually be +driven, and the guard in ``targets.py`` refuses the rest loudly. +""" + +from __future__ import annotations + +import os + +# What the reconstructed agent and the simulated user run on today. Both roles run constantly, so +# this is the setting worth revisiting first once the target can be handed to ALK. +AGENT = "claude-sonnet-4-6" +USER = "claude-sonnet-4-6" +# Kept separate and stronger. A judged sub-goal is the one place a cheap wrong answer is +# expensive: it decides a pass, it runs once per scenario, and nobody re-reads it. +JUDGE = "claude-opus-4-7" + + +def for_roles(override: str | None = None) -> dict[str, str]: + """The model each part runs on. + + ``override`` names one model for every role, which is what a caller comparing two models end + to end is asking for: same suite, same world, one thing changed. + """ + if override: + return {"agent": override, "user": override, "judge": override} + return { + "agent": os.environ.get("ALK_AGENT_MODEL", AGENT), + "user": os.environ.get("ALK_USER_MODEL", USER), + "judge": os.environ.get("ALK_JUDGE_MODEL", JUDGE), + } diff --git a/harness/src/agent_harness/run/platform_evals.py b/harness/src/agent_harness/run/platform_evals.py new file mode 100644 index 0000000..fe512de --- /dev/null +++ b/harness/src/agent_harness/run/platform_evals.py @@ -0,0 +1,224 @@ +"""Judged sub-goals as evals on the platform, created once and invoked per run. + +A sub-goal that nothing observable can settle is a sentence: "the agent explained why it could +not change the price, and did not invent a reason". That sentence is already the whole input a +custom eval wants, so rather than asking a model here and keeping the answer in a run folder, the +sentence becomes a named eval on the platform, created once when the world is built and invoked +after every run. + +What that buys, beyond tidiness: the eval is versioned and reusable, it shows up in the product +rather than only in our artifacts, and the same judgement can be applied to production traffic +later without being rewritten. The harness wrote it; it is theirs to keep. + +Deterministic checks stay as code. They are better as code, and nothing here should tempt anyone +to send a question a database can answer to a language model. +""" + +from __future__ import annotations + +import json +import os +import re +import time +from typing import Any + +# What the conversation is called inside an eval's instructions. Deliberately plain: the platform +# extracts variables from the instructions themselves, and the reserved roots (row, span, trace, +# session, call) would be swallowed. +CONVERSATION = "conversation" + +# Both are needed. Without them the harness falls back to judging here, rather than failing a run +# over a credential, because a suite that cannot run without a platform account is a worse tool. +KEYS = ("FI_API_KEY", "FI_SECRET_KEY") + +# The judge. A typo here does not raise: an unknown model silently falls back to this same value, +# so the only protection against sending the wrong one is sending the right one. +MODEL = "turing_large" + +# Names the platform accepts, and which are stable for the same sub-goal on the same agent, so +# that running a suite twice reuses one eval rather than making a second. +_ALLOWED = re.compile(r"[^a-z0-9_-]+") + + +def configured() -> bool: + return all(os.environ.get(name) for name in KEYS) + + +def eval_name(agent: str, sub_goal: str) -> str: + """A stable name for one agent's sub-goal. + + Includes the agent, because two agents can reasonably have a sub-goal called the same thing + and mean different questions by it. Uniqueness on the platform is per organisation, so a + bare `refused_clearly` would collide across every agent anybody tests. + """ + return _ALLOWED.sub("-", f"{agent}-{sub_goal}".lower()).strip("-")[:64] + + +def suite_eval_name(agent: str, eval_name: str) -> str: + """A stable, non-colliding platform name for a suite-wide evaluation.""" + return _ALLOWED.sub("-", f"{agent}-suite-{eval_name}".lower()).strip("-")[:64] + + +def judge_builtin(name: str, inputs: dict[str, str]) -> dict[str, Any]: + """Run a built-in eval by identifier, with its documented inputs.""" + from fi.evals import Evaluator + + answered = Evaluator().evaluate(eval_templates=name, inputs=inputs, model_name="turing_flash") + first = (getattr(answered, "eval_results", None) or [None])[0] + output = getattr(first, "output", None) + reason = getattr(first, "reason", "") or "" + if first is None or (output is None and reason): + raise RuntimeError(f"{name} did not run: {reason or 'no result'}") + return { + "output": output, + "why": reason, + "model": getattr(first, "model", None) or "turing_flash", + } + + +def instructions_for(claim: str, agent: str, rules: list[str] | None = None) -> str: + """The eval's own prompt: what to decide, and what to decide it from. + + One variable, carrying the whole record of the run rather than only what was said. Speech + alone cannot settle most of these: "the answer is correct" is decidable against what the + database actually returned and not against the sentence quoting it, and an eval given only + the transcript correctly reports that it has no way to tell. What the agent did is part of + the conversation in the sense that matters here. + + Everything else is fixed when the eval is created, because it is a fact about the agent + rather than about one run. + """ + known = ( + "\n\nThe agent under test is bound by these rules:\n - " + "\n - ".join(rules[:10]) + if rules + else "" + ) + return ( + f"You are judging one run of {agent}.\n\n" + f"Decide strictly: {claim}\n\n" + "You are given a JSON record of the run: what the person was asked to do, every tool " + "call the agent made with what came back, what was said, and the state of the world " + "afterwards.\n\n" + "The tool calls are the truth about what happened. An agent that says it did something " + "no call performed has not done it, however convincing it sounds, and an answer is " + "correct when it matches what the calls returned. A refused call did not happen: judge " + "what the agent ended up doing, not what it tried on the way. Something merely not " + "contradicted does not hold. Where the claim is that something must not have happened, " + "it holds when the thing did not happen. Declining something holds only if the agent " + "both declined it and gave a true reason; refusing while inventing a reason does not " + "hold." + f"{known}\n\n" + "The run:\n" + f"{{{{{CONVERSATION}}}}}" + ) + + +def ensure(name: str, claim: str, agent: str, rules: list[str] | None = None) -> bool: + """Create this eval if the platform does not already have it. True when it is there. + + Creation is checked against a list that is scoped to the workspace while uniqueness is + scoped to the organisation, so an eval made in a sibling workspace is invisible here and + creating it raises. That is not an error worth failing a run over: the eval exists, which is + all this needs to be true. + """ + from fi.evals import EvalTemplateManager + + manager = EvalTemplateManager() + wanted = instructions_for(claim, agent, rules) + found = manager.list_templates(search=name) + existing = next( + (one for one in getattr(found, "items", []) or [] if one.name == name), None + ) + if existing is not None: + # Same eval, kept at the same name and id, rather than a second one beside it. Its + # instructions are the harness's, so when those change the eval on the platform is + # behind: an old one silently judging new runs is the failure mode worth avoiding, and + # a new name every time would litter the account with near-duplicates. + if (getattr(existing, "instructions", "") or "") != wanted: + manager.update_template(existing.id, instructions=wanted, model=MODEL) + return True + try: + manager.create_template( + name=name, + instructions=wanted, + eval_type="llm", + model=MODEL, + output_type="pass_fail", + pass_threshold=0.5, + # A draft cannot be run, and nothing later says why. + is_draft=False, + tags=["harness", "sub-goal"], + ) + except Exception as refused: # noqa: BLE001 - the one failure that means success + if "already exists" not in str(refused).lower(): + raise + return True + + +def judge(name: str, record: dict[str, Any], *, tries: int = 5) -> dict[str, Any]: + """Run one eval over one run, and give back what it decided. + + The record is JSON-encoded rather than pasted. Rendering is sandboxed Jinja, and agents write + code blocks: a run containing braces would otherwise be read as template syntax and either + explode or quietly render as something else. + """ + from fi.evals import Evaluator + + payload = json.dumps(record, ensure_ascii=False, indent=2, default=str) + for attempt in range(max(1, tries)): + try: + # The model is named again here. The template carries one, but the run does not + # inherit it: without model_name the request arrives as "Model 'None'" and is + # refused, which is a 400 rather than anything about the conversation. + answered = Evaluator().evaluate( + eval_templates=name, + inputs={CONVERSATION: payload}, + model_name=MODEL, + ) + break + except Exception as failed: # noqa: BLE001 - retried only when told to wait + after = _retry_after(failed) + if after is None and attempt + 1 >= tries: + raise + # Rate limiting is organisation-wide, so a suite running scenarios at once is + # exactly the shape that trips it, and the client does not back off on its own. + time.sleep(after if after is not None else 2.0**attempt) + else: # pragma: no cover - the loop either breaks or raises + raise RuntimeError(f"{name} did not answer") + + first = (getattr(answered, "eval_results", None) or [None])[0] + output = getattr(first, "output", None) + reason = getattr(first, "reason", "") or "" + # An eval that did not run is not an eval that failed. The SDK reports a rejected request by + # handing back a result whose output is empty and whose reason is the error, and reading that + # as "the claim does not hold" would fail an agent for an expired key or a bad payload. It + # raises instead, and the caller falls back to judging locally. + if first is None or (output is None and reason): + raise RuntimeError(f"{name} did not run: {reason or 'no result'}") + return { + "held": _passed(output), + "why": reason, + "output": output, + "eval": name, + # Present but null on a result, so a plain getattr default never fires. + "model": getattr(first, "model", None) or MODEL, + } + + +def _retry_after(failed: Exception) -> float | None: + """How long the platform asked us to wait, when that is what it said.""" + response = getattr(failed, "response", None) + headers = getattr(response, "headers", None) or {} + try: + return float(headers.get("Retry-After")) + except (TypeError, ValueError): + return None + + +def _passed(output: Any) -> bool: + """Whether a verdict is a pass, given it can arrive as a word or a number.""" + if isinstance(output, bool): + return output + if isinstance(output, (int, float)): + return float(output) >= 0.5 + return str(output).strip().lower() in ("pass", "passed", "true", "yes") diff --git a/harness/src/agent_harness/run/simulation.py b/harness/src/agent_harness/run/simulation.py new file mode 100644 index 0000000..7c30600 --- /dev/null +++ b/harness/src/agent_harness/run/simulation.py @@ -0,0 +1,602 @@ +"""The one thing the harness hands a suite to. + +The harness does not run scenarios. It builds a world, writes scenarios against it, and calls +`simulate` once. Everything after that belongs to ALK: how many run at a time, whether the person +is typed to or phoned, where the audio goes, what a report looks like. + +That split matters more than it looks. While the harness ran scenarios itself, one at a time, +through its own conversation loop, a suite was only as good as the harness's patience: a run took +as many turns of the chat as it had scenarios, and the simulator driving it was not the one the +product ships. Handing over means the suite runs the same way whether a person triggered it from +the UI, a script did, or nobody did. + +Chat and voice are one path here, and they differ in exactly one respect the harness never sees: +a chat agent runs in this process and reaches the world as an object, while a hosted voice agent +runs in somebody else's cloud and reaches the same world over HTTP. Same world, same setup, same +checks, same report. Only the wire differs, and the spec's ``world_kind`` decides it. + +A run is a folder. One simulation over a suite is one run, kept whole, so a session accumulates +runs that can be compared rather than one result file that the next run overwrites. +""" + +from __future__ import annotations + +import asyncio +import json +import time +from dataclasses import asdict +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Callable + +from ..contract import AgentContract +from ..scenario import Scenario +from .grade import Result + +RUNS = "runs" +RUN = "run.json" +RESULT = "result.json" +TRANSCRIPT = "transcript.txt" +CALLS = "calls.json" + +# How many scenarios run at once by default. One, because the shipped default should be the one +# that cannot surprise anybody: a voice suite places real calls that cost real money, and fanning +# out to twenty is a bad thing to learn from a bill. +CONCURRENCY = 1 + +# What ALK calls the world and the person, per modality. Both are registry names it validates +# against the plugin's own manifest, so a typo is an error here rather than a confusing run. +WORLDS = {"text": ("chat", "chat"), "voice": ("voice", "voice")} +SIMULATORS = {"text": "synthetic_user", "voice": "livekit_simulator"} + + +def spoken_to(contract: AgentContract) -> bool: + """Whether this agent is spoken to rather than typed to.""" + return (contract.modality or "text").strip().lower() == "voice" + + +def new_run_id() -> str: + return datetime.now(timezone.utc).strftime("run-%Y%m%d-%H%M%S") + + +def run_root(destination: Path, run_id: str) -> Path: + return Path(destination) / RUNS / run_id + + +def every_run(destination: Path) -> list[dict[str, Any]]: + """Every run in this session, newest first, finished or not. + + A run that is still going is reported too, from the results already written. `run.json` is + written once, at the end, so requiring it meant an hour-long suite showed nothing at all + while its results sat on disk: the scenario that finished forty minutes ago was as invisible + as the one that had not started. `finished` says which kind each is. + """ + root = Path(destination) / RUNS + if not root.exists(): + return [] + found: list[dict[str, Any]] = [] + for folder in sorted(root.iterdir(), reverse=True): + if not folder.is_dir(): + continue + kept = folder / RUN + if kept.exists(): + try: + summary = json.loads(kept.read_text(encoding="utf-8")) + except Exception: # noqa: BLE001 - one unreadable run never hides the rest + continue + summary["finished"] = True + found.append(summary) + continue + done = _cases_so_far(folder) + if done: + found.append( + { + "run_id": folder.name, + "finished": False, + "scenarios": len(done), + "passed": sum(1 for one in done if one.get("passed")), + "seconds": round(sum(one.get("seconds") or 0 for one in done), 1), + "results": done, + } + ) + return found + + +def _cases_so_far(folder: Path) -> list[dict[str, Any]]: + """The scenarios of an unfinished run that have already been written.""" + done: list[dict[str, Any]] = [] + for case in sorted(folder.iterdir()): + kept = case / RESULT + if not case.is_dir() or not kept.exists(): + continue + try: + one = json.loads(kept.read_text(encoding="utf-8")) + except Exception: # noqa: BLE001 - a result being written this instant is not an error + continue + done.append( + { + "scenario": one.get("scenario", case.name), + "passed": bool(one.get("passed")), + "met": one.get("met"), + "of": len(one.get("checkpoints") or []), + "seconds": one.get("seconds"), + "recording": one.get("recording", ""), + "problems": one.get("problems") or [], + } + ) + return done + + +def read_run(destination: Path, run_id: str) -> dict[str, Any]: + """One run in full: its summary, and every scenario's result, transcript and calls. + + Read from the folder rather than held in memory, so the harness can be asked about a run + that happened before it was started, and about any single call inside one. + """ + root = run_root(destination, run_id) + kept = root / RUN + if not root.exists(): + raise FileNotFoundError(f"no run {run_id} in {destination}") + # A run still going has no summary yet, but the scenarios it has finished are readable and + # worth reading. Only a folder that is not there at all is an error. + summary = ( + json.loads(kept.read_text(encoding="utf-8")) + if kept.exists() + else {"run_id": run_id, "finished": False, "passed": 0} + ) + summary.setdefault("finished", kept.exists()) + scenarios: list[dict[str, Any]] = [] + for folder in sorted(root.iterdir()): + if not folder.is_dir() or not (folder / RESULT).exists(): + continue + one = json.loads((folder / RESULT).read_text(encoding="utf-8")) + one["transcript"] = _text(folder / TRANSCRIPT) + one["calls_detail"] = _json(folder / CALLS) + scenarios.append(one) + summary["scenarios"] = scenarios + return summary + + +def _text(path: Path) -> str: + return path.read_text(encoding="utf-8") if path.exists() else "" + + +def _json(path: Path) -> list[dict[str, Any]]: + return json.loads(path.read_text(encoding="utf-8")) if path.exists() else [] + + +async def simulate( + scenarios: list[Scenario], + contract: AgentContract, + world_root: Path, + *, + destination: Path | None = None, + model: str | None = None, + concurrency: int = CONCURRENCY, + run_id: str = "", + on_case_start: Callable[[Scenario], Any] | None = None, + on_case_done: Callable[[Result], Any] | None = None, +) -> dict[str, Any]: + """Run a whole suite through ALK and write it out as one run. + + Returns the run's summary. Results are in the order they were asked for, not the order they + finished, so a report reads the same however it was scheduled. + """ + from .models import for_roles + + destination = Path(destination or world_root) + run_id = run_id or new_run_id() + root = run_root(destination, run_id) + root.mkdir(parents=True, exist_ok=True) + roles = for_roles(model) + + started = time.time() + room = asyncio.Semaphore(max(1, concurrency)) + ordered: list[Result | None] = [None] * len(scenarios) + + async def one(index: int, scenario: Scenario) -> None: + async with room: + if on_case_start: + on_case_start(scenario) + began = time.time() + folder = root / scenario.name + folder.mkdir(parents=True, exist_ok=True) + try: + result = await _run_one( + scenario, contract, world_root, folder, roles=roles + ) + except Exception as failed: # noqa: BLE001 - one bad scenario never stops the suite + result = Result( + scenario=scenario.name, + problems=[f"{type(failed).__name__}: {failed}"], + ) + result.seconds = round(time.time() - began, 1) + _write_case(folder, result) + ordered[index] = result + if on_case_done: + on_case_done(result) + + await asyncio.gather( + *(one(index, scenario) for index, scenario in enumerate(scenarios)) + ) + results = [one for one in ordered if one is not None] + + summary = { + "run_id": run_id, + "agent": contract.agent, + "modality": contract.modality or "text", + "started": datetime.now(timezone.utc).isoformat(timespec="seconds"), + "seconds": round(time.time() - started, 1), + "concurrency": concurrency, + "models": roles, + "scenarios": len(results), + "passed": sum(1 for one in results if one.passed), + "spent_usd": round(sum(one.spent_usd for one in results), 4), + # Averaged across the scenarios that reported them, so a suite has one line per metric + # rather than a number nobody compares. Only over the runs that actually measured it: + # averaging a missing metric as zero would make a suite look worse the more of it failed + # to run, which is the opposite of informative. + "metrics": _averaged([one.measured for one in results]), + "results": [ + { + "scenario": one.scenario, + "passed": one.passed, + "met": one.met, + "of": len(one.checkpoints), + "seconds": one.seconds, + "recording": one.recording, + "problems": one.problems, + } + for one in results + ], + } + (root / RUN).write_text( + json.dumps(summary, indent=2, default=str), encoding="utf-8" + ) + return summary + + +def _write_case(folder: Path, result: Result) -> None: + """One scenario's result, transcript and calls, each in the form it is read in. + + The transcript is written as text because it is read by people, and the calls as JSON + because they are read by the UI and by the harness looking into a single call. + """ + body = asdict(result) + body["passed"] = result.passed + body["met"] = result.met + detail = body.pop("calls_detail", None) or [] + (folder / RESULT).write_text( + json.dumps(body, indent=2, default=str), encoding="utf-8" + ) + (folder / TRANSCRIPT).write_text(result.transcript or "", encoding="utf-8") + (folder / CALLS).write_text( + json.dumps(detail, indent=2, default=str), encoding="utf-8" + ) + + +async def _run_one( + scenario: Scenario, + contract: AgentContract, + world_root: Path, + folder: Path, + *, + roles: dict[str, str], +) -> Result: + """One scenario, in its own world, through ALK's runner. + + The world is prepared here and handed in, rather than named in the spec, because isolation + is ours to guarantee: every scenario starts from the same frozen base with only its own + setup applied, and a world shared between cases would let the first one decide what the + second is graded against. + """ + + from ..folder import apply_setup, check_ready + from ..world.snapshot import restore + + spoken = spoken_to(contract) + kind = "voice" if spoken else "text" + adapter, world_kind = WORLDS[kind] + + world = restore(world_root) + try: + world.reset() + applied = apply_setup(scenario, world) + if not applied.ok: + raise RuntimeError(f"the scenario's setup did not run: {applied.said}") + ready = check_ready(scenario, world) + if not ready.ok: + raise RuntimeError( + f"the world is not ready for this scenario: {ready.said}. Running it would " + "test us rather than the agent." + ) + # The setup's own calls are not the agent's. + world.calls = [] + + if not spoken: + # Typed, and driven by a model rather than by ALK's chat simulator. + # + # That simulator is deterministic on purpose: an untyped persona gets three fixed + # lines ("Can you give me the exact next step…"), and a typed one renders utterances + # from a compiled behaviour policy. Reproducible, and not a simulation of a person. + # A suite whose user says the same three things to every agent tests one path and + # calls it coverage. + # + # So the conversation is driven here, by a model reading the simulator prompt the + # build stage wrote for this agent. Everything around it is unchanged: same world, + # same setup, same checks, same run folder. + return await _typed_to(scenario, contract, world, world_root, folder, roles=roles) + + # Spoken. The agent is not here: it runs in Vapi, with its own prompt, its own model + # and its own voice, and the only thing that changes is where its tools are answered. + # ALK places the call and drives a simulated caller that is a real model over STT and + # TTS, so this half was never deterministic. + return await _spoken_to( + scenario, contract, world, world_root, folder, roles=roles + ) + finally: + world.close() + + +def _found_audio(directory: Path) -> Path | None: + """The recording a run left behind, if it left one. + + Asked of the directory rather than taken on trust from whatever placed the call: a runner + that exits badly still returns a path, and a path is not a file. + """ + if not directory.exists(): + return None + for path in sorted(directory.rglob("*")): + if path.is_file() and path.suffix.lower() in (".wav", ".mp3", ".ogg", ".m4a"): + return path + return None + + +async def _typed_to( + scenario: Scenario, + contract: AgentContract, + world: Any, + world_root: Path, + folder: Path, + *, + roles: dict[str, str], +) -> Result: + """A typed conversation, with a model on both sides. + + The same grading as every other run: the world it is handed is already set up, and what it + leaves behind is what the checks read. + """ + from ..catalogue import load_catalogue + from . import converse + from .grade import checkpoints, grade_sub_goals, judge, judge_suite_evals + from .targets import resolve + + agent = resolve("local")(contract, world, model=roles["agent"]) + transcript = await converse( + agent, scenario, contract, world_root=world_root, model=roles["user"] + ) + catalogue = load_catalogue(world_root) + settled = grade_sub_goals(world, scenario, catalogue, transcript.calls) + ending = ", ".join( + f"{name}: {len(rows)} rows" for name, rows in sorted(world.observe().state.items()) + ) + judgements, judged_cost = await judge( + scenario, transcript, contract, catalogue, model=roles["judge"], ending=ending + ) + judgements += judge_suite_evals( + catalogue.suite_evals, scenario, transcript, contract, ending=ending + ) + result = Result( + scenario=scenario.name, + tests=scenario.tests, + state_failures=[f"{one.name}: {one.said}" for one in settled if not one.held], + conduct=judgements, + checkpoints=checkpoints(settled, judgements), + crashes=[f"{call.name}: {call.error}" for call in transcript.crashed()], + ended=transcript.ended, + turns=len(transcript.exchanges), + calls=len(transcript.calls), + spent_usd=transcript.spent_usd + judged_cost, + transcript=transcript.spoken(), + actions=transcript.actions(), + ) + result.calls_detail = _calls_of(transcript.calls) + return result + + +def _calls_of(calls: Any) -> list[dict[str, Any]]: + """Every call in full, for the timeline and for anyone asking what one call did.""" + return [ + { + "name": call.name, + "arguments": call.arguments, + "result": str(call.result)[:2000], + "ok": call.ok, + "refused": call.refused, + "error": call.error, + "at": getattr(call, "at", 0.0), + } + for call in calls + ] + + +async def _spoken_to( + scenario: Scenario, + contract: AgentContract, + world: Any, + world_root: Path, + folder: Path, + *, + roles: dict[str, str], +) -> Result: + """A real call, with the agent's own tools answered by this world. + + The agent under test is not reconstructed here and is not running in this process. It is the + hosted assistant, with its own prompt, model and voice; the only thing that changes for the + duration is where its tool calls are sent. That makes this the more faithful of the two + paths, and the reason a spoken suite is worth more than a typed one. + + The call itself belongs to ALK, which drives a simulated caller through speech: a real model + behind STT and TTS, not a script. + """ + import os + import time + + from ..catalogue import load_catalogue + from .call import place_the_call + from .conversation import Exchange, Transcript + from .grade import checkpoints, grade_sub_goals, judge, judge_suite_evals + from .live import wire + from .evidence import measured, newest_report, tracks_in + from .tools import missing_prerequisites + + stopping = missing_prerequisites() + if stopping: + raise RuntimeError("cannot place a call:\n - " + "\n - ".join(stopping)) + + def placed() -> tuple[Any, str, str]: + """Everything about the call, off the event loop. + + Wiring reads a subprocess's stdout and the call itself blocks for minutes. Run inline + they freeze whatever loop is hosting this, which for the UI means the stream, the status + endpoint and the stop button all stop with it. + """ + _world, instruction, webhook, tunnel, _url, _moved = wire( + scenario, world_root, world=world + ) + started = time.time() + try: + os.environ["HARNESS_INSTRUCTION"] = instruction + os.environ["HARNESS_SCENARIO"] = scenario.name + os.environ["HARNESS_OUTCOME"] = scenario.tests + code = place_the_call(os.environ.get("HARNESS_VOICE_CASE", "2.1.2")) + finally: + webhook.stop() + if tunnel is not None: + tunnel.terminate() + # Everything the runner recorded about this call, read from the report it wrote. + return code, newest_report(started) + + code, case = await asyncio.to_thread(placed) + spoken = str(case.get("transcript") or "") + # Every track that exists, copied in beside the result so a run is self-contained and the + # page can fall back when the preferred one is missing. + kept = _keep_tracks(tracks_in(case), folder) + + catalogue = load_catalogue(world_root) + settled = grade_sub_goals(world, scenario, catalogue, world.calls) + # Judged the same way a typed run is. Without this a spoken scenario reports "1/2" when what + # happened is that one check passed and the other was never asked, which reads as the agent + # half-failing rather than as the suite not having looked. + spoken_transcript = Transcript( + exchanges=[ + Exchange("agent" if line.lower().startswith("assistant") else "customer", line) + for line in spoken.splitlines() + if line.strip() + ], + calls=list(world.calls), + ended="finished", + ) + judgements, judged_cost = await judge( + scenario, + spoken_transcript, + contract, + catalogue, + model=roles["judge"], + ending=", ".join( + f"{name}: {len(rows)} rows" + for name, rows in sorted(world.observe().state.items()) + ), + ) + judgements += judge_suite_evals( + catalogue.suite_evals, + scenario, + spoken_transcript, + contract, + ending=", ".join( + f"{name}: {len(rows)} rows" + for name, rows in sorted(world.observe().state.items()) + ), + ) + result = Result( + scenario=scenario.name, + tests=scenario.tests, + state_failures=[f"{one.name}: {one.said}" for one in settled if not one.held], + conduct=judgements, + checkpoints=checkpoints(settled, judgements), + spent_usd=judged_cost, + ended="finished", + turns=len([line for line in spoken.splitlines() if line.strip()]), + calls=len(world.calls), + transcript=spoken, + recording=(kept[0]["path"] if kept else ""), + ) + result.tracks = kept + result.measured = measured(case) + result.calls_detail = _calls_of(world.calls) + if code != 0 and not world.calls: + # A call that failed and never reached the world says nothing about the agent, and must + # not be recorded as the agent failing. + result.problems.append( + f"the voice runner exited {code} and no tool call reached the world" + ) + return result + + +def _keep_tracks(found: list[dict[str, str]], folder: Path) -> list[dict[str, str]]: + """Copy each recording into this run's folder, keeping the order it was offered in. + + Copied rather than referenced, because the runner's own directory is transient and a run + that cannot be listened to next week is a run that cannot be shown to anybody. + """ + import shutil + + folder.mkdir(parents=True, exist_ok=True) + kept: list[dict[str, str]] = [] + for track in found: + source = Path(track["path"]) + if not source.exists(): + continue + landed = folder / f"{track['label'].replace(':', '_')}{source.suffix}" + try: + shutil.copyfile(source, landed) + except OSError: + continue + kept.append({"label": track["label"], "path": str(landed)}) + return kept + + +def _averaged(measured: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Each metric's mean over the scenarios that reported it, carrying whether it applied. + + A metric that had nothing to measure scores 1.0, so averaging the lot produces a suite + summary in which two thirds of the numbers are perfect and none of them mean anything. The + applicability travels with the average instead of being flattened away, so a reader is never + shown "browser action safety 1.00" for a suite of phone calls without also being told there + were no browser actions. + """ + gathered: dict[str, list[float]] = {} + applies: dict[str, bool] = {} + reasons: dict[str, str] = {} + for one in measured: + for metric in (one or {}).get("metrics") or []: + name, value = metric.get("name"), metric.get("score") + if not name or not isinstance(value, (int, float)): + continue + gathered.setdefault(name, []).append(float(value)) + # Applicable anywhere is applicable: one scenario exercising a capability is enough + # to make the number worth reading across the suite. + applies[name] = applies.get(name, False) or bool(metric.get("applicable", True)) + if metric.get("reason") and name not in reasons: + reasons[name] = str(metric["reason"]) + return [ + { + "name": name, + "score": round(sum(values) / len(values), 4), + "applicable": applies.get(name, True), + "reason": reasons.get(name, ""), + "cases": len(values), + } + for name, values in sorted(gathered.items()) + if values + ] diff --git a/harness/src/agent_harness/run/stage.py b/harness/src/agent_harness/run/stage.py new file mode 100644 index 0000000..b355f0b --- /dev/null +++ b/harness/src/agent_harness/run/stage.py @@ -0,0 +1,101 @@ +"""Stage four: run the scenarios against the real agent, and say what came back. + +The last stage that was a command rather than a conversation. Nothing about it needed to be: +wiring the world to the assistant and running the checks is already code, and the part worth +having judgement on is which scenario to run and what a failure actually means. + +That second part is why this is a stage at all. A failing check has four possible causes and only +one of them is a finding about the agent — the others are a wrong check, a wrong contract, or a +simulated caller that never asked for the thing. Deciding which is reading, not arithmetic. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Callable + +from claude_agent_sdk import ClaudeAgentOptions + +from ..config import ( + artifact_dir, + UNWANTED, + gate_hooks, + chosen_model, + load_skill, + permission_gate, + provider_env, +) +from ..contract import AgentContract +from ..scenario_tools import load_scenarios +from ..session import Stage +from ..tools import qualified +from .tools import RUN_SERVER, TOOL_NAMES, load_results, missing_prerequisites, run_tools + +SKILL = "run-scenarios" + + +def open_stage( + contract: AgentContract, + *, + out: Path | None = None, + ask: Callable[..., Any] | None = None, + max_turns: int = 40, +) -> tuple[Stage, Path]: + """A live run-the-scenarios stage, and where it will write its results.""" + destination = out or artifact_dir(contract.agent) + server = run_tools(destination, destination, contract=contract) + allowed = [ + "AskUserQuestion", + *(qualified(RUN_SERVER, name) for name in TOOL_NAMES), + ] + options = ClaudeAgentOptions( + system_prompt=( + f"{load_skill(SKILL)}\n\n## This agent\n\n{contract.brief()}" + ), + allowed_tools=allowed, + mcp_servers={RUN_SERVER: server}, + permission_mode="default", + cwd=str(destination.parent if destination.parent.exists() else Path.cwd()), + setting_sources=[], + max_turns=max_turns, + model=chosen_model(), + env=provider_env(), + ) + options.disallowed_tools = list(UNWANTED) + options.hooks = gate_hooks(allowed) + options.can_use_tool = permission_gate(ask, allowed) + return Stage(options, name=SKILL), destination + + +def opening(contract: AgentContract, destination: Path) -> str: + """What to tell the stage when it opens. + + Deliberately does not tell it to run everything. Each call costs money and takes minutes, and + a stage that opens by spending the whole suite gives nobody a chance to say which one they + cared about. + """ + written = load_scenarios(destination) + already = load_results(destination) + blocked = missing_prerequisites() if contract.modality == "voice" else [] + if blocked: + return ( + f"There are {len(written)} scenarios for {contract.agent!r}, but a live call cannot " + "be placed yet:\n - " + "\n - ".join(blocked) + "\n\nSay this plainly and stop." + ) + if already: + passed = sum(1 for record in already if record["passed"]) + return ( + f"{len(already)} of {len(written)} scenarios for {contract.agent!r} have been run, " + f"{passed} passing. Say where things stand with read_results, then ask which to run." + ) + return ( + f"{len(written)} scenarios are ready for {contract.agent!r} and none has been run.\n\n" + "Run preflight, then list_scenarios, then say which ones you would run first and why. " + "Do not start running them until you are asked to — each call takes minutes and costs " + "real money." + ) + + +def load(destination: Path) -> list[dict[str, Any]]: + """What has been run for this agent, if anything has.""" + return load_results(Path(destination)) diff --git a/harness/src/agent_harness/run/targets.py b/harness/src/agent_harness/run/targets.py new file mode 100644 index 0000000..62bd17b --- /dev/null +++ b/harness/src/agent_harness/run/targets.py @@ -0,0 +1,242 @@ +"""What is being tested, and how the harness talks to it. + +The rest of the run does not care what the agent under test is. It says something and gets a +reply back, and whatever tool calls happened in between landed in the world. That is the entire +interface, and keeping it that narrow is what lets the same scenarios, the same world and the +same grading run against an agent hosted anywhere. + +Two things are supplied per target: how to say something to it, and how its tool calls reach the +world. ``LocalAgent`` runs the agent in this process from its contract, which needs nothing +except the contract and is what makes a suite runnable the moment the world is built. A hosted +target is the same class with the transport swapped: the agent runs wherever it runs, its tool +calls arrive over a webhook, and the webhook answers from ``world.handle_tool_call``. The world +does not change, the scenarios do not change, and the grading does not change. +""" + +from __future__ import annotations + +from typing import Any, Callable, Protocol, runtime_checkable + +from claude_agent_sdk import ClaudeAgentOptions, create_sdk_mcp_server, tool + +from ..config import ( + UNWANTED, + chosen_model, + gate_hooks, + permission_gate, + provider_env, +) +from ..contract import AgentContract +from ..session import Stage +from ..tools import qualified +from ..world.runtime import GeneratedWorld + +AGENT_SERVER = "agent" + +_TYPES: dict[str, type] = { + "str": str, + "string": str, + "int": int, + "integer": int, + "float": float, + "number": float, + "bool": bool, + "boolean": bool, + "list": list, + "dict": dict, +} + + +def _python_type(declared: str) -> type: + """The type a tool's argument is declared with, as something a schema can carry.""" + lowered = (declared or "").strip().lower() + if lowered.startswith(("list", "sequence", "array")): + return list + if lowered.startswith(("dict", "mapping", "object")): + return dict + return _TYPES.get(lowered, str) + + +def describe(spec: Any, contract: AgentContract) -> str: + """What the agent is told a tool takes, including the values it accepts. + + The values matter more than they look. An agent whose real schema enumerates its menu knows + that a Big Mac combo is ``big_mac_combo``; the same agent without them guesses, gets refused, + and reads as broken when what is broken is the harness that withheld them. Anything the + contract recorded as permitted, the agent under test is told. + """ + parts = [spec.description or f"{spec.name} for {contract.agent}"] + for arg in spec.args: + values = spec.arg_values.get(arg) + if isinstance(values, (list, tuple)) and values: + rendered = ", ".join(str(value) for value in values) + parts.append(f" {arg} accepts: {rendered}") + elif arg in spec.arg_types: + parts.append(f" {arg}: {spec.arg_types[arg]}") + return "\n".join(parts) + + +def agent_tools(contract: AgentContract, world: GeneratedWorld) -> Any: + """The agent's own tools, wired to the world so a call really happens. + + Every call goes through ``world.call``, so a refusal comes back as a refusal the agent can + read and recover from, rather than as a success it will happily build on. + """ + + def bind(spec: Any) -> Any: + schema = { + arg: _python_type(spec.arg_types.get(arg, "str")) for arg in spec.args + } + + @tool(spec.name, describe(spec, contract), schema) + async def call_tool( + args: dict[str, Any], _name: str = spec.name + ) -> dict[str, Any]: + # Through handle_tool_call, not straight to world.call. That method is the interface + # ALK's own runners drive an environment by, so going around it would leave the + # claim that a generated world plugs into them untested — and free to drift. + done = world.handle_tool_call({"name": _name, "arguments": args}) + if done is None: + return { + "content": [{"type": "text", "text": f"no such tool {_name}"}], + "is_error": True, + } + return { + "content": [{"type": "text", "text": done.content or ""}], + **({} if done.success else {"is_error": True}), + } + + return call_tool + + return create_sdk_mcp_server( + name=AGENT_SERVER, + version="0.1.0", + tools=[bind(spec) for spec in contract.tools], + ) + + +def agent_prompt(contract: AgentContract) -> str: + """The agent under test, as its contract describes it. + + Only what the contract records, because anything added here is a difference between the agent + being graded and the agent that exists. + """ + parts = [ + f"You are {contract.agent}: {contract.one_liner}".strip(), + contract.system_prompt_excerpt.strip(), + ] + if contract.hard_constraints: + parts.append( + "Rules you must follow:\n - " + "\n - ".join(contract.hard_constraints) + ) + if contract.modality == "voice": + parts.append( + "You are speaking out loud. Keep replies to what a person would actually say: " + "short, no lists, no markdown." + ) + parts.append( + "Use your tools to do anything real. Never tell the customer something is done unless a " + "tool confirmed it, and if a tool refuses, say so plainly and offer what is possible." + ) + return "\n\n".join(part for part in parts if part) + + +@runtime_checkable +class Target(Protocol): + """An agent under test, reachable by saying something to it.""" + + key: str + + async def open(self) -> None: ... + async def say(self, utterance: str) -> str: ... + async def close(self) -> None: ... + @property + def spent_usd(self) -> float: ... + + +def _drivable(model: str | None) -> None: + """Refuse a model this target cannot actually run, before a suite is graded on it. + + This target runs on the Claude Agent SDK against Vertex, so the only models it can drive are + Anthropic's. Handed anything else it does not fail: it produces a session that answers + nothing, which arrives as a scenario with no turns and no calls and every check red. That + reads exactly like an agent that ignored the person, and the whole suite is wrong in a way + nobody would think to question. + """ + named = (model or "").strip().lower() + if not named or "claude" in named or named.startswith("anthropic"): + return + raise RuntimeError( + f"this target cannot run {model!r}. It drives the agent through the Claude Agent SDK on " + "Vertex, which speaks to Anthropic models only. To run the agent on something else, " + "point the spec's target at one of ALK's own endpoint adapters rather than at this one." + ) + + +class LocalAgent: + """The agent run here, from its contract, with its tools bound to the world.""" + + key = "local" + + def __init__( + self, + contract: AgentContract, + world: GeneratedWorld, + *, + model: str | None = None, + max_turns: int = 12, + ) -> None: + self.contract = contract + self.world = world + _drivable(model) + allowed = [qualified(AGENT_SERVER, spec.name) for spec in contract.tools] + options = ClaudeAgentOptions( + system_prompt=agent_prompt(contract), + allowed_tools=allowed, + mcp_servers={AGENT_SERVER: agent_tools(contract, world)}, + permission_mode="default", + setting_sources=[], + max_turns=max_turns, + model=chosen_model(model), + env=provider_env(model), + ) + # The agent under test gets its own tools and nothing else. A target that can reach a + # file or a shell is not the agent anybody deployed. + options.disallowed_tools = list(UNWANTED) + options.hooks = gate_hooks(allowed) + options.can_use_tool = permission_gate(granted=allowed) + self._stage = Stage(options, name="target") + + async def open(self) -> None: + await self._stage.__aenter__() + + async def say(self, utterance: str) -> str: + turn = await self._stage.say(utterance) + return turn.text.strip() + + async def close(self) -> None: + await self._stage.__aexit__(None, None, None) + + @property + def spent_usd(self) -> float: + return self._stage.spent_usd + + +_REGISTRY: dict[str, Callable[..., Target]] = {LocalAgent.key: LocalAgent} + + +def register_target(key: str, factory: Callable[..., Target]) -> None: + """Add a way of reaching an agent. A hosted runtime is a class and this line.""" + _REGISTRY[key] = factory + + +def resolve(key: str) -> Callable[..., Target]: + if key not in _REGISTRY: + raise NotImplementedError( + f"no target {key!r}; registered targets are {', '.join(sorted(_REGISTRY))}" + ) + return _REGISTRY[key] + + +def supported() -> tuple[str, ...]: + return tuple(sorted(_REGISTRY)) diff --git a/harness/src/agent_harness/run/tools.py b/harness/src/agent_harness/run/tools.py new file mode 100644 index 0000000..231d153 --- /dev/null +++ b/harness/src/agent_harness/run/tools.py @@ -0,0 +1,476 @@ +"""The tools that run a scenario against the real agent, and record what happened. + +Placing a call was a command before this existed, which made the last stage the only one you +could not simply ask for. Nothing about it needed to be a command: wiring the world to the +assistant and grading afterwards is already code, and choosing which scenario to run and reading +what came back is the part worth having judgement on. + +So the same shape as every other stage. The tools do what must be exact — restore the world, +repoint the assistant's own tools, place the call through ALK, run the checks — and the stage +decides what to run and says what it means. + +A run takes minutes, not seconds. The tool blocks for that long, and says so, because a stage +that fires a call and returns immediately would report on a conversation that has not happened. +""" + +from __future__ import annotations + +import asyncio +import json +import os +import shutil +import time +from pathlib import Path +from typing import Any + +from claude_agent_sdk import create_sdk_mcp_server, tool + +from ..catalogue import load_catalogue +from ..scenario_tools import load_scenarios +from ..tools import schema +from .call import place_the_call +from .live import LiveRun, grade, wire + +RUN_SERVER = "runs" +RESULTS = "runs.json" + +# What a hosted agent needs before a call can be placed at all. Checked up front rather than +# three minutes in, because the failure otherwise arrives after the expensive part. +REQUIRED = ("VAPI_API_KEY", "VAPI_ASSISTANT_ID") + + +def _ok(text: str) -> dict[str, Any]: + return {"content": [{"type": "text", "text": text}]} + + +def _err(text: str) -> dict[str, Any]: + return {"content": [{"type": "text", "text": text}], "is_error": True} + + +def missing_prerequisites() -> list[str]: + """What would stop a live call, in the words of what to do about it.""" + problems: list[str] = [] + absent = [name for name in REQUIRED if not os.environ.get(name)] + if absent: + problems.append( + f"{', '.join(absent)} not set. The assistant already exists with the agent's own " + "tools; without these there is no way to reach it. Load the env file first:\n" + " set -a; . ./.env.acceptance; set +a" + ) + if not os.environ.get("HARNESS_WEBHOOK_URL") and not shutil.which("cloudflared"): + problems.append( + "no way to expose the webhook publicly. A hosted agent cannot reach loopback, so " + "either install cloudflared (brew install cloudflared) or set HARNESS_WEBHOOK_URL " + "to a tunnel that is already running." + ) + return problems + + +def save_results(results: list[dict[str, Any]], destination: Path) -> Path: + """Keep every run, so a suite can be read after the fact rather than scrolled back to.""" + destination = Path(destination) + destination.mkdir(parents=True, exist_ok=True) + path = destination / RESULTS + path.write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding="utf-8") + return path + + +def load_results(destination: Path) -> list[dict[str, Any]]: + path = Path(destination) / RESULTS + if not path.exists(): + return [] + try: + loaded = json.loads(path.read_text(encoding="utf-8")) + return loaded if isinstance(loaded, list) else [] + except json.JSONDecodeError: + return [] + + +def as_record(run: LiveRun) -> dict[str, Any]: + return { + "scenario": run.scenario, + "passed": bool(run.settled) and run.met == len(run.settled) and not run.problems, + "met": run.met, + "of": len(run.settled), + "settled": [ + {"name": one.name, "held": one.held, "said": one.said, "broken": one.broken} + for one in run.settled + ], + "judged": list(run.judged), + "calls": list(run.calls), + "problems": list(run.problems), + } + + +def transcript_since(started: float) -> str: + """What was said on the call that just happened, from the voice runner's own report. + + The voice case owns the call and writes its report where it always has; reaching into that + report is how the transcript gets onto the run record without the harness re-implementing + any of the call. Only a report written after this run started counts — the newest file on + disk is otherwise last week's call wearing today's verdict. + """ + root = ARTIFACTS_ROOT / "simulation-acceptance" + if not root.exists(): + return "" + newest: tuple[float, Path] | None = None + for report in root.glob("run_*/*/report.json"): + written = report.stat().st_mtime + if written >= started and (newest is None or written > newest[0]): + newest = (written, report) + if newest is None: + return "" + try: + loaded = json.loads(newest[1].read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return "" + for result in loaded.get("results") or []: + spoken = result.get("transcript") + if isinstance(spoken, str) and spoken.strip(): + return spoken + return "" + + +def report(run: LiveRun) -> str: + """One run, as something worth reading rather than a score.""" + lines = [run.line()] + lines += [one.line() for one in run.settled] + lines += [f" [?] {name} — judged, not settled by code" for name in run.judged] + if run.problems: + lines += [f" !! {problem}" for problem in run.problems] + lines.append("") + lines.append("what the agent actually did:") + lines += [f" {call}" for call in run.calls or ["(no tool calls reached the world)"]] + return "\n".join(lines) + + +def run_tools( + world_root: Path, + destination: Path, + *, + contract: Any = None, + case: str = "", +) -> Any: + """A server for running one agent's scenarios against the real thing. + + How a scenario runs is decided by what the agent is, not by this stage. A hosted voice agent + gets the live path — its own tools repointed at the world over a webhook, the call placed + through ALK. Anything else runs here: the agent stood up from its contract, conversing over + the same world, graded by the same checks. The scenarios, the world and the grading are + identical either way; only the transport changes. + """ + written = load_scenarios(destination) + catalogue = load_catalogue(destination) + results = load_results(destination) + voice_case = case or os.environ.get("HARNESS_VOICE_CASE", "2.1.2") + live = bool(contract is not None and getattr(contract, "modality", "") == "voice") + + @tool( + "list_scenarios", + "The scenarios that can be run, what each one tests, and which of its sub-goals are " + "settled by code rather than left to a judge.", + schema({}, []), + ) + async def list_scenarios(_args: dict[str, Any]) -> dict[str, Any]: + if not written: + return _err("no scenarios have been written for this agent yet") + lines: list[str] = [] + for one in written: + settled = [ + name + for name in one.sub_goals + if (found := catalogue.named(name)) and found.deterministic() + ] + judged = [name for name in one.sub_goals if name not in settled] + ran = next((r for r in results if r["scenario"] == one.name), None) + mark = "" if ran is None else (" [last run: PASS]" if ran.get("passed") else " [last run: FAIL]") + lines.append( + f"{one.name}{mark}\n tests: {one.tests or one.use_case or '—'}\n" + f" settled by code: {', '.join(settled) or 'none'}\n" + f" judged: {', '.join(judged) or 'none'}" + ) + return _ok("\n".join(lines)) + + @tool( + "preflight", + "Check everything a run needs before spending one. For a hosted voice agent that is the " + "assistant's credentials and a way to expose the webhook publicly; for anything else " + "the run happens here and needs nothing external. Run this before the first run.", + schema({}, []), + ) + async def preflight(_args: dict[str, Any]) -> dict[str, Any]: + if not live: + return _ok( + "Ready. This agent runs here, against the world, from its contract — nothing " + f"external is needed. {len(written)} scenarios are available." + ) + problems = missing_prerequisites() + if problems: + return _err("Not ready:\n - " + "\n - ".join(problems)) + return _ok( + "Ready. Credentials are set and the webhook can be exposed. " + f"{len(written)} scenarios are available." + ) + + async def _run_here(scenario: Any) -> dict[str, Any]: + """The scenario against the agent stood up from its contract, over the same world.""" + from . import run_suite + + if contract is None: + return _err("no contract is loaded, so there is no agent to stand up") + graded = await run_suite([scenario], contract, world_root, out=destination) + results[:] = load_results(destination) + result = graded[0] + lines = [result.line()] + [check.line() for check in result.checkpoints] + if result.transcript: + lines += ["", "the conversation:", result.transcript] + answer = "\n".join(lines) + return _ok(answer) if result.passed else _err(answer) + + @tool( + "run_simulation", + "Run the whole suite. One call: every scenario, each in its own copy of the world, " + "graded, and written out as one run you can come back to.\n\n" + "This is how a suite is run. Running scenarios one at a time is for looking into a " + "single failure afterwards, not for getting results.\n\n" + "`concurrency` is how many run at once. Leave it at 1 for a spoken agent, where every " + "scenario is a real call. It takes minutes and blocks until the whole suite is done.", + schema({"concurrency": int, "model": str}, []), + ) + async def run_simulation(args: dict[str, Any]) -> dict[str, Any]: + from .simulation import simulate + + if contract is None: + return _err("no contract is loaded, so there is no agent to run against") + if not written: + return _err("there are no scenarios to run") + summary = await simulate( + list(written), + contract, + world_root, + destination=destination, + model=str(args.get("model") or "") or None, + concurrency=max(1, int(args.get("concurrency") or 1)), + ) + results[:] = load_results(destination) + lines = [ + f"{summary['run_id']}: {summary['passed']}/{summary['scenarios']} passed " + f"in {summary['seconds']}s, ${summary['spent_usd']}", + "", + ] + for one in summary["results"]: + mark = "PASS" if one["passed"] else "FAIL" + note = f" {one['problems'][0]}" if one["problems"] else "" + audio = " [recording]" if one["recording"] else "" + lines.append( + f" {mark} {one['scenario']} {one['met']}/{one['of']}{audio}{note}" + ) + lines += [ + "", + "read_run gives any one of these in full: the conversation, every tool call with " + "its arguments, and what each check decided.", + ] + return _ok("\n".join(lines)) + + @tool( + "read_run", + "One run in full, or the list of runs when no id is given. A run holds every scenario's " + "conversation, every tool call with its arguments and result, and what each check " + "decided — which is what a failure is diagnosed from.", + schema({"run_id": str, "scenario": str}, []), + ) + async def read_run(args: dict[str, Any]) -> dict[str, Any]: + from .simulation import every_run, read_run as load_run + + run_id = str(args.get("run_id") or "") + if not run_id: + runs = every_run(destination) + if not runs: + return _ok("No runs yet. run_simulation makes one.") + return _ok( + "\n".join( + f" {one['run_id']} {one.get('passed', 0)}/{one.get('scenarios', 0)} " + f"passed {one.get('seconds', 0)}s" + for one in runs + ) + ) + try: + whole = load_run(destination, run_id) + except FileNotFoundError as missing: + return _err(str(missing)) + wanted = str(args.get("scenario") or "") + cases = [ + one + for one in whole.get("scenarios", []) + if not wanted or one.get("scenario") == wanted + ] + if not cases: + return _err(f"{run_id} has no scenario called {wanted!r}") + return _ok(json.dumps(cases if wanted else whole, indent=2, default=str)[:6000]) + + @tool( + "run_scenario", + "Run one scenario against the agent and grade it.\n\n" + "The world is restored and the scenario's setup applied first. A hosted voice agent is " + "reached live — its OWN tools are pointed at the world over a webhook and the call is " + "placed; any other agent is stood up here from its contract and conversed with. Either " + "way the sub-goals' checks run against what the world holds afterwards plus the calls " + "that were made.\n\n" + "It can take minutes and blocks until the run is over. Run one at a time and read what " + "comes back before running the next.", + # Both spellings accepted: every model that has driven this stage has guessed + # `scenario` at least once, and a retry on an argument name is a wasted turn. + schema({"name": str, "scenario": str}, []), + ) + async def run_scenario(args: dict[str, Any]) -> dict[str, Any]: + name = str(args.get("name") or args.get("scenario") or "") + scenario = next((one for one in written if one.name == name), None) + if scenario is None: + return _err( + f"no scenario called {name!r}. There is: " + + ", ".join(one.name for one in written) + ) + if not live: + return await _run_here(scenario) + problems = missing_prerequisites() + if problems: + return _err( + "Cannot place a call:\n - " + + "\n - ".join(problems) + + "\nThis is the environment this harness is running in, not something to fix " + "in the scenario." + ) + + def placed() -> tuple[LiveRun, str, list[str], str]: + """The whole call, off the event loop. + + Wiring reads a subprocess's stdout and placing the call blocks for minutes; run + inline they freeze whatever loop is hosting this tool, which for the web UI means + the stream, the status endpoint and the stop button all die for the duration. + """ + world, instruction, webhook, tunnel, url, moved = wire(scenario, world_root) + started = time.time() + try: + # The caller's instruction reaches the voice case through the environment, so + # how a simulated caller behaves is not decided in two places. + os.environ["HARNESS_INSTRUCTION"] = instruction + os.environ["HARNESS_SCENARIO"] = scenario.name + os.environ["HARNESS_OUTCOME"] = scenario.tests + code = place_the_call(voice_case) + run = grade(scenario, world, world_root) + if code != 0 and not run.calls: + run.problems.append( + f"the voice runner exited {code} and no tool call reached the world, " + "so this says nothing about the agent" + ) + finally: + webhook.stop() + if tunnel is not None: + tunnel.terminate() + world.close() + return run, url, moved, transcript_since(started) + + run, url, moved, spoken = await asyncio.to_thread(placed) + + record = as_record(run) + record["instruction"] = scenario.instruction + record["transcript"] = spoken + # Re-read before writing: the local suite writes the same file, and a list loaded when + # this stage opened would silently roll back anything recorded since. + results[:] = [ + r for r in load_results(destination) if r.get("scenario") != scenario.name + ] + results.append(record) + save_results(results, destination) + answer = f"webhook: {url}/tool\nrepointed: {', '.join(moved)}\n\n{report(run)}" + return _ok(answer) if not run.problems else _err(answer) + + @tool( + "read_results", + "What every scenario did the last time it was run, without running anything.", + schema({}, []), + ) + async def read_results(_args: dict[str, Any]) -> dict[str, Any]: + if not results: + return _ok("nothing has been run yet") + lines = [] + for record in results: + mark = "PASS" if record.get("passed") else "FAIL" + # Two record shapes share this file: live runs carry settled/judged, local runs + # carry checkpoints. Both say what failed, and both deserve to be read. + failed = [ + f"{one.get('name')}: {one.get('said') or one.get('detail') or ''}" + for one in (record.get("settled") or record.get("checkpoints") or []) + if not (one.get("held") if "held" in one else one.get("passed")) + ] + met = record.get("met", record.get("checkpoints_met", "?")) + of = record.get("of") + scored = f"{met}/{of}" if of is not None else str(met) + lines.append( + f"{mark} {record.get('scenario')} {scored}" + + ("\n - " + "\n - ".join(failed) if failed else "") + ) + passed = sum(1 for record in results if record.get("passed")) + return _ok("\n".join(lines) + f"\n\n{passed} of {len(results)} passed") + + server = create_sdk_mcp_server( + name=RUN_SERVER, + version="0.1.0", + tools=[ + list_scenarios, + preflight, + run_simulation, + read_run, + run_scenario, + read_results, + ], + ) + return server + + +TOOL_NAMES = ( + "list_scenarios", + "preflight", + "run_simulation", + "read_run", + "run_scenario", + "read_results", +) + + +# Which of the several recordings a call leaves behind is the one worth keeping. Both sides on +# one track, because the question asked of a spoken run is nearly always about the interaction: +# whether the agent talked over the caller, how long it left them waiting, what it heard. +PREFERRED = ("_stereo.wav", "stereo.wav", "combined.wav") + + +def recording_since(started: float, into: Path) -> str: + """Copy the audio from the call that just happened into this run's folder. + + ALK records already and writes several tracks under its own artifacts directory. Rather than + tell it where to put them — which it takes from its manifest, not from the environment — the + files it wrote are found the same way the transcript is, by being newer than the moment this + run began, and the one worth keeping is copied in beside the result. + """ + root = ARTIFACTS_ROOT / "simulation-acceptance" + if not root.exists(): + return "" + fresh = [ + path + for path in root.rglob("*") + if path.is_file() + and path.suffix.lower() in (".wav", ".mp3", ".ogg") + and path.stat().st_mtime >= started + ] + if not fresh: + return "" + chosen = next( + (one for mark in PREFERRED for one in fresh if one.name.endswith(mark)), + max(fresh, key=lambda one: one.stat().st_size), + ) + into = Path(into) + into.mkdir(parents=True, exist_ok=True) + landed = into / f"recording{chosen.suffix}" + shutil.copyfile(chosen, landed) + return str(landed) +from ..config import ARTIFACTS_ROOT diff --git a/harness/src/agent_harness/run/voice.py b/harness/src/agent_harness/run/voice.py new file mode 100644 index 0000000..f984921 --- /dev/null +++ b/harness/src/agent_harness/run/voice.py @@ -0,0 +1,205 @@ +"""Serving a real voice agent's tool calls from a generated world. + +A hosted voice agent executes its tools by calling a webhook. So the whole integration is one +thing: stand up that webhook, and answer it from the world instead of from canned responses. + +That single swap is what the environment was built for. The previous run's known issues were all +the same defect wearing different clothes: + +- *"Mocked tools always succeed, including removing an item that was never added."* +- *"Mock responses do not vary by argument, so read-after-write flows are wrong."* +- *"World state does not change unless a scenario sets state_updates, which is often empty."* + +A world that really holds rows and can really refuse answers all three, because the reply the +agent hears is produced by running the call rather than by looking it up. + +Nothing here decides pass or fail. Grading reads the world afterwards and the calls this server +recorded, through the same sub-goal checks every other run uses. +""" + +from __future__ import annotations + +import json +import logging +import os +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer +from typing import Any, Mapping + +from ..world.runtime import GeneratedWorld + +logger = logging.getLogger(__name__) + +VAPI_API = os.environ.get("VAPI_API_BASE_URL", "https://api.vapi.ai").rstrip("/") + +# Vapi's edge rejects the default urllib User-Agent with a 403 that says nothing about why, while +# the identical request from curl succeeds. Sending one is the whole fix. +_AGENT = "alk-harness/0.1" + + +class WorldWebhook: + """The webhook a hosted agent calls, answered by a generated world. + + One world at a time. ``bind`` swaps which world is live between scenarios, so the assistant + stays configured while every scenario still starts from its own restored copy. + """ + + def __init__(self, host: str = "127.0.0.1", port: int = 0) -> None: + self._world: GeneratedWorld | None = None + self._lock = threading.Lock() + try: + server = HTTPServer((host, port), _handler_for(self)) + except OSError: + # A leftover server from an earlier run must not block this one; any free port works + # because the public URL is discovered after binding. + logger.warning("port %s busy, binding an ephemeral port instead", port) + server = HTTPServer((host, 0), _handler_for(self)) + self._server = server + self.port = server.server_address[1] + self._thread = threading.Thread(target=server.serve_forever, daemon=True) + + def start(self) -> "WorldWebhook": + self._thread.start() + logger.info("world webhook listening on port %s", self.port) + return self + + def stop(self) -> None: + self._server.shutdown() + self._server.server_close() + + def bind(self, world: GeneratedWorld) -> None: + """Make one world live. Its own call log is what grading reads afterwards.""" + with self._lock: + self._world = world + world.reset() + + @property + def calls(self) -> list[Any]: + with self._lock: + return list(self._world.calls) if self._world else [] + + def respond(self, name: str, arguments: Mapping[str, Any]) -> str: + """Answer one tool call by running it. + + A refusal is returned as the answer, not as an error: the agent has to hear "that item is + unavailable" and cope with it, which is the whole reason the world can say no. What it + must never hear is an acknowledgement for something that did not happen. + """ + with self._lock: + world = self._world + if world is None: + return "the environment is not ready" + + done = world.handle_tool_call({"name": name, "arguments": dict(arguments)}) + if done is None: + return f"there is no tool called {name}" + return done.content or ("done" if done.success else "that could not be done") + + +def _handler_for(owner: "WorldWebhook"): + class Handler(BaseHTTPRequestHandler): + def log_message(self, *args: Any) -> None: # silence per-request stderr noise + return + + def do_POST(self) -> None: # noqa: N802 - required name + length = int(self.headers.get("Content-Length") or 0) + raw = self.rfile.read(length) if length else b"{}" + try: + payload = json.loads(raw or b"{}") + except json.JSONDecodeError: + payload = {} + results = [ + {"toolCallId": call_id, "result": owner.respond(name, arguments)} + for call_id, name, arguments in tool_calls(payload) + ] + body = json.dumps({"results": results}).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + return Handler + + +def tool_calls(payload: Mapping[str, Any]) -> list[tuple[str, str, dict[str, Any]]]: + """Pull (id, name, arguments) out of a provider's tool-call webhook body.""" + message = payload.get("message") or payload + raw = message.get("toolCalls") or message.get("toolCallList") or [] + found: list[tuple[str, str, dict[str, Any]]] = [] + for entry in raw if isinstance(raw, list) else []: + if not isinstance(entry, Mapping): + continue + function = entry.get("function") or {} + name = str(function.get("name") or entry.get("name") or "") + arguments = function.get("arguments") or entry.get("arguments") or {} + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError: + arguments = {"_raw": arguments} + if name: + found.append((str(entry.get("id") or ""), name, dict(arguments))) + return found + + +def pointed_at(tools: list[dict[str, Any]], webhook_url: str) -> list[dict[str, Any]]: + """The agent's own tools, with only where they are answered changed. + + The assistant under test already has its tools — the names, the arguments, the enums are the + agent's, defined by whoever built it. Redefining them here would mean testing an agent we + wrote rather than theirs, and any drift between the two would show up as a finding about + them. So nothing is rebuilt: the one thing that changes is the address the call goes to. + """ + repointed: list[dict[str, Any]] = [] + for tool in tools: + moved = json.loads(json.dumps(tool)) + moved.setdefault("server", {})["url"] = f"{webhook_url.rstrip('/')}/tool" + repointed.append(moved) + return repointed + + +def fetch_assistant(assistant_id: str, api_key: str) -> dict[str, Any]: + """The assistant as it stands, so its own tools can be read rather than guessed.""" + import urllib.request + + request = urllib.request.Request( + f"{VAPI_API}/assistant/{assistant_id}", + headers={"Authorization": f"Bearer {api_key}", "User-Agent": _AGENT}, + ) + with urllib.request.urlopen(request, timeout=20) as answer: + return json.loads(answer.read()) + + +def repoint_assistant( + assistant_id: str, api_key: str, webhook_url: str +) -> list[str]: + """Send the assistant's existing tool calls to our webhook. Returns the tools moved.""" + import urllib.request + + assistant = fetch_assistant(assistant_id, api_key) + tools = (assistant.get("model") or {}).get("tools") or [] + if not tools: + raise RuntimeError( + f"assistant {assistant_id} has no tools, so there is nothing for the environment " + "to answer. It is the agent's own tools that get repointed, not ones we add." + ) + model = json.loads(json.dumps(assistant.get("model") or {})) + model["tools"] = pointed_at(tools, webhook_url) + + body = json.dumps({"model": model}).encode() + request = urllib.request.Request( + f"{VAPI_API}/assistant/{assistant_id}", + data=body, + method="PATCH", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "User-Agent": _AGENT, + }, + ) + with urllib.request.urlopen(request, timeout=20) as answer: + answer.read() + return [ + str((one.get("function") or {}).get("name") or "") for one in model["tools"] + ] diff --git a/harness/src/agent_harness/scenario.py b/harness/src/agent_harness/scenario.py new file mode 100644 index 0000000..27650aa --- /dev/null +++ b/harness/src/agent_harness/scenario.py @@ -0,0 +1,228 @@ +"""A scenario: a delta on the base environment, and what must hold afterwards. + +The base is built once — the world, the simulator's prompt, the catalogue of sub-goals. A +scenario changes a few values in that world, fills the prompt's slots, and names which sub-goals +must hold. It is not a template with values slotted into it; the harness writes each one. + +It also carries a **solution**: what a correct agent would do. That is not decoration. It is what +proves, before the scenario is ever used, that the scenario can be passed at all and that its +checks are not vacuous — the two gates in ``prove.py``. Terminal-bench keeps its tasks honest the +same way, and it needs no model to do it. +""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + +from .catalogue import Catalogue +from .simulator import variables_in + + +class Step(BaseModel): + """One action in a reference solution.""" + + tool: str + arguments: dict[str, Any] = Field(default_factory=dict) + + +class Persona(BaseModel): + """The simulated caller, in the same shape used by existing voice scenarios. + + A persona controls how the caller pursues a scenario's task. The task itself remains on + ``Scenario.instruction`` so the harness can vary either one without conflating them. + """ + + name: str = "" + gender: str = "" + age_group: str = "" + occupation: str = "" + location: str = "" + personality: str = "" + communication_style: str = "" + keywords: list[str] = Field(default_factory=list) + languages: list[str] = Field(default_factory=list) + accent: str = "" + multilingual: bool = False + metadata: dict[str, Any] = Field(default_factory=dict) + + def described(self) -> bool: + return bool( + self.name + or self.gender + or self.age_group + or self.occupation + or self.location + or self.personality + or self.communication_style + or self.keywords + or self.languages + or self.accent + or self.metadata + ) + + def missing_profile_fields(self) -> list[str]: + """The minimum needed for a scenario to exercise caller variation intentionally.""" + missing = [ + name + for name, value in ( + ("name", self.name), + ("personality", self.personality), + ("communication_style", self.communication_style), + ("accent", self.accent), + ) + if not value.strip() + ] + if not self.languages: + missing.append("languages") + if not self.keywords: + missing.append("keywords") + return missing + + def format_persona(self) -> str: + """A stable, human-readable profile the simulator can consistently embody.""" + parts = [] + identity = [] + for label, value in ( + ("Name", self.name), + ("Gender", self.gender), + ("Age Group", self.age_group), + ("Occupation", self.occupation), + ("Location", self.location), + ): + if value: + identity.append(f"- {label}: {value}") + if identity: + parts.append("# YOUR IDENTITY\n\n" + "\n".join(identity)) + + behavior = [] + if self.personality: + behavior.append(f"- Personality: {self.personality}") + if self.communication_style: + behavior.append(f"- Communication Style: {self.communication_style}") + if self.keywords: + behavior.append("- Key Traits: " + ", ".join(self.keywords)) + if behavior: + parts.append("# YOUR PERSONALITY & COMMUNICATION\n\n" + "\n".join(behavior)) + + speech = [] + if self.languages: + speech.append("- Language(s): " + ", ".join(self.languages)) + if self.accent: + speech.append(f"- Accent: {self.accent}") + if self.multilingual: + speech.append("- Switch languages naturally when the conversation calls for it.") + if speech: + parts.append("# LANGUAGE & SPEECH PATTERNS\n\n" + "\n".join(speech)) + + if self.metadata: + characteristics = [ + f"- {key.replace('_', ' ').title()}: {value}" + for key, value in self.metadata.items() + ] + parts.append("# ADDITIONAL CHARACTERISTICS\n\n" + "\n".join(characteristics)) + return "\n".join(parts) + + +class Scenario(BaseModel): + """One test: what changes, what is asked, what a correct agent does, what must hold.""" + + name: str + use_case: str = "" + tests: str = "" + + # What this scenario changes about the world after it is reset, as code: a file defining + # ``setup(world)``. Rows in a table were enough while every world was a database, and they + # are not enough now — a scenario may need a service to start returning errors, a file to be + # missing, a queue to be backed up. Code can express all of that; a table of rows cannot. + setup_code: str = "" + + # Whether the world is actually ready for this scenario, as code: a file defining + # ``ready(world)`` that answers with nothing when the world holds what this scenario + # presumes, or a sentence saying what is missing. + # + # This is the precondition, and it is the difference between a real finding and a wasted + # run: a scenario about the last five chocolates is only a test of the agent if there really + # are five. Otherwise the agent fails for something we got wrong, and it looks like the + # agent's fault. + ready_code: str = "" + + # The task. For a conversational agent it fills the simulator prompt's instruction slot; for + # a browser or coding agent it goes to the agent directly. + instruction: str = "" + # Who is making the request. This is deliberately separate from the task so a caller's + # communication needs do not get buried in an unstructured instruction. + persona: Persona | None = None + # Anything else that prompt asks for, by slot name. + variables: dict[str, str] = Field(default_factory=dict) + + # What a correct agent would do. Run by the gates, never by the agent under test. + solution: list[Step] = Field(default_factory=list) + + # Which entries of the shared catalogue must hold. Named, not restated, so results roll up + # across the suite: the same sub-goal failing in seven of twelve scenarios is one sentence. + sub_goals: list[str] = Field(default_factory=list) + + max_turns: int = 10 + + def slots(self) -> dict[str, str]: + """Every value this scenario offers the simulator prompt.""" + persona = {"persona": self.persona.format_persona()} if self.persona else {} + return {"instruction": self.instruction, **self.variables, **persona} + + +def validate_scenario( + scenario: Scenario, + catalogue: Catalogue, + world_state: dict[str, list[dict[str, Any]]], + simulator_prompt: str = "", +) -> list[str]: + """Problems that make a scenario unusable, found without running anything. + + Whether it can actually be passed is a different question, and no amount of reading settles + it. That is what the gates are for. + """ + problems: list[str] = [] + if not scenario.name.strip(): + problems.append("no name") + if not scenario.instruction.strip(): + problems.append("no instruction: there is nothing for the run to be about") + if scenario.persona is not None and not scenario.persona.described(): + problems.append("persona has no details") + elif scenario.persona is not None and (missing := scenario.persona.missing_profile_fields()): + problems.append("persona is incomplete: " + ", ".join(missing)) + if not scenario.sub_goals: + problems.append( + "no sub_goals: nothing would be graded. Name the entries of the catalogue this " + "scenario is meant to exercise" + ) + + unknown = sorted(set(scenario.sub_goals) - catalogue.names()) + if unknown: + problems.append( + f"sub_goals not in the catalogue: {', '.join(unknown)}. Use the shared names, or add " + f"them to the catalogue first. It has: {', '.join(sorted(catalogue.names())) or 'none'}" + ) + + # setup_code and ready_code are not read here. Whether they work is not a question reading + # them can answer, and running them is exactly what the first gate does. + if scenario.setup_code.strip() and "def setup(" not in scenario.setup_code: + problems.append("setup_code must define setup(world)") + if scenario.ready_code.strip() and "def ready(" not in scenario.ready_code: + problems.append("ready_code must define ready(world)") + + if simulator_prompt: + unfilled = sorted(variables_in(simulator_prompt) - set(scenario.slots())) + if unfilled: + problems.append( + f"the simulator prompt asks for {', '.join(unfilled)}, which this scenario does " + "not supply. An unfilled slot reaches the caller verbatim" + ) + + if not scenario.solution: + problems.append( + "no solution: without the actions a correct agent would take, there is no way to " + "show this scenario can be passed at all" + ) + return problems diff --git a/harness/src/agent_harness/scenario_tools.py b/harness/src/agent_harness/scenario_tools.py new file mode 100644 index 0000000..da4beba --- /dev/null +++ b/harness/src/agent_harness/scenario_tools.py @@ -0,0 +1,601 @@ +"""The tools that write scenarios, and the gates that decide one may be kept. + +A scenario is accepted by being *proved*, not by looking right. ``submit_scenario`` puts it +through three gates, in order: the world must end up holding what the scenario presumes, the +reference solution must pass the scenario's own checks, and those same checks must fail when +nothing is done at all. + +Every gate is code. No model is asked whether a scenario is good; the environment decides. A +scenario that clears all three is written out as its own folder of runnable files. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from claude_agent_sdk import create_sdk_mcp_server, tool + +from .amend import add_rule, drop_rule, fix_tool, widen +from .contract import AgentContract +from .catalogue import Catalogue, SubGoal, load_catalogue, save_catalogue, validate_sub_goal +from .simulator import load_simulator_prompt +from .folder import SCENARIOS, apply_setup, read_all, write_folder, write_index +from .prove import prepared, prove +from .scenario import Scenario, validate_scenario +from .tools import brief, schema +from .world.snapshot import restore + +SCENARIO_SERVER = "scenarios" + + +def _ok(text: str) -> dict[str, Any]: + return {"content": [{"type": "text", "text": text}]} + + +def _err(text: str) -> dict[str, Any]: + return {"content": [{"type": "text", "text": text}], "is_error": True} + + +def write_scenarios( + scenarios: list[Scenario], destination: Path, catalogue: Catalogue | None = None +) -> Path: + """Write every scenario out as its own folder, and regenerate the index over them.""" + catalogue = catalogue if catalogue is not None else load_catalogue(destination) + for one in scenarios: + write_folder(one, catalogue, destination) + _forget_dropped(scenarios, destination) + return write_index(scenarios, destination) + + +def _forget_dropped(scenarios: list[Scenario], destination: Path) -> None: + """Remove the folders of scenarios that are no longer in the suite. + + The folders are the truth, and they are what gets read back. Writing the survivors without + taking the others away means a dropped scenario returns on the next load, still failing, and + dropping it appears to do nothing at all. + """ + import shutil + + root = Path(destination) / SCENARIOS + if not root.exists(): + return + keeping = {one.name for one in scenarios} + for folder in root.iterdir(): + if folder.is_dir() and folder.name not in keeping: + shutil.rmtree(folder) + + +def load_scenarios(destination: Path) -> list[Scenario]: + """Every scenario on disk, read from its folder. + + The folders are the truth. The index beside them is regenerated from these, so it can + describe them but never contradict them. + """ + return read_all(destination) + + +def accept_scenario( + payload: dict[str, Any], + *, + world_root: Path, + catalogue: Catalogue, + kept: list[Scenario], + simulator_prompt: str = "", +) -> dict[str, Any]: + """Validate one scenario, then prove it. A plain function so both halves are testable.""" + try: + scenario = Scenario.model_validate(payload) + except Exception as invalid: + return _err(f"Not kept. {invalid}"[:600]) + + # Read against the world this scenario actually runs in, so a setup that creates the table + # a check reads is not reported as referring to something that does not exist. + trial, _applied, _ready = prepared(scenario, world_root) + try: + problems = validate_scenario(scenario, catalogue, trial.state(), simulator_prompt) + finally: + trial.close() + + if problems: + return _err("Not kept. Fix these and submit again:\n - " + "\n - ".join(problems)) + + proof = prove(scenario, catalogue, world_root) + if not proof.holds: + said = f"Not kept. {proof.why()}" + # Code written against the wrong collection shape is the commonest way setup, ready and a + # check fail here, and the exception alone does not say which collections are mappings and + # which are lists. The world is asked, so the answer names them. + if "attribute" in said.lower() or "not subscriptable" in said.lower(): + world = restore(world_root) + try: + said += f"\n\n{world.shapes()}" + finally: + world.close() + return _err(said) + + replaced = any(one.name == scenario.name for one in kept) + kept[:] = [one for one in kept if one.name != scenario.name] + kept.append(scenario) + weak = ( + "\nWorth tightening: " + + ", ".join(proof.weak) + + " still held with nothing done. The scenario is graded by its other checks, so it was " + "kept, but those sub-goals will report themselves as held for an agent that did nothing. " + "A check that asserts the attempt, not only the state it leaves, cannot do that." + if proof.weak + else "" + ) + return _ok( + f"{scenario.name} {'replaced' if replaced else 'kept'}. All three gates pass: the world " + "is ready for it, the reference solution passes its checks, and those checks fail when " + f"nothing is done.{weak}\n{len(kept)} so far: " + ", ".join(one.name for one in kept) + ) + + +def not_ready(kept: list[Scenario], wanted: int, catalogue: Catalogue) -> list[str]: + """Why this suite is not worth saving yet.""" + problems: list[str] = [] + if len(kept) < wanted: + problems.append( + f"{len(kept)} of the {wanted} asked for. The ones that find something are usually " + "the awkward ones, so this is worth finishing rather than stopping here. If nobody " + f"asked for {wanted}, record what they did ask for with aim_for." + ) + elif len(kept) > wanted: + problems.append( + f"{len(kept)} scenarios against a target of {wanted}. If they asked for more, " + "aim_for records the new size; reopening a suite starts with the target set to what " + "is already there, so adding to one always reads like this. If you wrote extra " + "nobody asked for, drop_scenario takes them off." + ) + # Two scenarios claiming the same use case are either the same test twice, or one of them is + # mislabelled. Both happened in the same suite: a delivered-order refusal was filed under + # "cancel a pending order", which is neither what it tests nor distinguishable afterwards + # from the scenario that really does test that. A use case is how coverage is counted, so a + # duplicate quietly overstates it. + claimed: dict[str, list[str]] = {} + for one in kept: + case = (one.use_case or "").strip().lower() + if case: + claimed.setdefault(case, []).append(one.name) + for case, names in claimed.items(): + if len(names) > 1: + problems.append( + f"{' and '.join(names)} both claim the use case {case!r}. Give each the use case " + "it actually exercises, or drop the one that duplicates the other. Coverage is " + "counted by use case, so two scenarios sharing one hides a gap." + ) + + # Sub-goals are shared so results roll up. A suite where every scenario invents its own is a + # suite whose results cannot be added together. + used = [name for one in kept for name in one.sub_goals] + if kept and len(used) > 2 and len(set(used)) == len(used): + problems.append( + "no sub-goal is used by more than one scenario, so nothing rolls up across the " + "suite. Reuse the catalogue where the same thing is being checked." + ) + return problems + + +def scenario_tools( + contract: AgentContract, world_root: Path, destination: Path, *, wanted: int +) -> tuple[Any, list[Scenario]]: + """A server for writing scenarios against one built environment.""" + kept: list[Scenario] = load_scenarios(destination) + catalogue = load_catalogue(destination) + simulator_prompt = load_simulator_prompt(destination) + target = {"count": wanted} + + scenario_required = ["name", "instruction", "solution", "sub_goals"] + if contract.conversational: + scenario_required.append("persona") + + @tool( + "inspect_world", + "Look at what is in the world. Without a table, lists the tables and how many rows each " + "holds; with one, returns rows from it. `matching` is plain text, not SQL.", + schema({"table": str, "limit": int, "matching": str}, []), + ) + async def inspect_world(args: dict[str, Any]) -> dict[str, Any]: + world = restore(world_root) + try: + state = world.state() + table = str(args.get("table") or "") + if not table: + lines = [f"{n}: {len(r)} rows" for n, r in sorted(state.items())] + if catalogue.sub_goals: + lines.append( + "\nsub-goals available: " + ", ".join(sorted(catalogue.names())) + ) + return _ok("\n".join(lines) or "this world has no tables") + if table not in state: + return _err(f"no table {table!r}; this world has {', '.join(sorted(state))}") + rows = state[table] + matching = str(args.get("matching") or "").strip() + if matching: + needle = matching.lower() + found = [r for r in rows if needle in json.dumps(r, default=str).lower()] + if not found: + return _ok( + f"nothing in {table} contains {matching!r}, but it holds {len(rows)} rows." + ) + rows = found + shown = rows[: int(args.get("limit") or 20)] + return _ok( + f"{len(rows)} rows, showing {len(shown)}:\n" + + "\n".join(json.dumps(r, default=str) for r in shown) + ) + finally: + world.close() + + @tool( + "try_calls", + "Run calls against a throwaway copy of the world and see the state they leave. Use it to " + "work out a scenario's solution and what its checks should assert.\n\n" + "`setup_code` is optional: pass the same code you intend to give the scenario and the " + "calls run against a world it has already changed, so you can see what the agent would " + "actually face. Nothing is saved.", + schema({"calls": list, "setup_code": str}, ["calls"]), + ) + async def try_calls(args: dict[str, Any]) -> dict[str, Any]: + world = restore(world_root) + try: + world.reset() + trial = Scenario(name="trial", setup_code=str(args.get("setup_code") or "")) + applied = apply_setup(trial, world) + if not applied.ok: + return _err(f"the setup did not run: {applied.said}") + world.calls = [] + lines: list[str] = [] + for step in args.get("calls") or []: + if not isinstance(step, dict): + return _err("each call must be an object with a tool and arguments") + call = world.call(str(step.get("tool") or ""), step.get("arguments") or {}) + if call.refused: + lines.append(f"{call.name}: refused — {call.error}") + elif not call.ok: + lines.append(f"{call.name}: CRASHED — {call.error}") + else: + lines.append(f"{call.name}: ok — {brief(call.result)}") + state = world.state() + lines.append( + "state afterwards: " + + ", ".join(f"{n}.count={len(r)}" for n, r in sorted(state.items())) + ) + for name, rows in sorted(state.items()): + if rows and len(rows) <= 6: + lines.append(f"{name}: " + brief(rows, limit=1200)) + return _ok("\n".join(lines) or "no calls were made") + finally: + world.close() + + @tool( + "add_sub_goal", + "Add a named thing this agent can be checked on, shared by every scenario that needs it. " + "`check` is Python: define check(world, calls) returning a sentence when something is " + "wrong, or None when it held. `world` is the environment afterwards; `calls` is every " + "tool call made, each with .name, .arguments, .ok and .refused — so a check can insist a " + "call happened with the right arguments, not merely that it happened.\n\n" + "Use `judged` only where nothing observable settles it, saying what a model must decide " + "and why code cannot.", + schema({"name": str, "what": str, "check": str, "judged": str}, ["name", "what"]), + ) + async def add_sub_goal(args: dict[str, Any]) -> dict[str, Any]: + sub_goal = SubGoal( + name=str(args.get("name") or ""), + what=str(args.get("what") or ""), + check=str(args.get("check") or ""), + judged=str(args.get("judged") or ""), + ) + problems = validate_sub_goal(sub_goal) + if problems: + return _err("Not added:\n - " + "\n - ".join(problems)) + catalogue.sub_goals = [one for one in catalogue.sub_goals if one.name != sub_goal.name] + catalogue.sub_goals.append(sub_goal) + save_catalogue(catalogue, destination) + return _ok( + f"{sub_goal.name} added" + + ("" if sub_goal.deterministic() else " (judged, not deterministic)") + + f". The catalogue has {len(catalogue.sub_goals)}: " + + ", ".join(sorted(catalogue.names())) + ) + + @tool( + "submit_scenario", + "Keep one scenario. It is put through three gates before it is kept, and told which one " + "failed if any does:\n" + " 1. ready — the world is restored, setup_code runs, then ready_code. The world " + "must end up holding what this scenario presumes.\n" + " 2. solvable — the reference solution is played through that world and the checks of " + "every sub-goal named must pass.\n" + " 3. not vacuous — the same checks run again with nothing done at all, and must fail.\n\n" + "A scenario that clears all three is written out as its own folder of runnable files.", + schema( + { + "name": { + "type": "string", + "description": "Short identifier, lower case with hyphens or underscores. " + "It becomes this scenario's folder name.", + }, + "use_case": { + "type": "string", + "description": "Which of the agent's use cases this belongs to.", + }, + "tests": { + "type": "string", + "description": "One line: what this scenario is trying to find out.", + }, + "instruction": { + "type": "string", + "description": "The task, written to the person the agent is serving. For a " + "conversational agent this fills the simulator prompt's slot.", + }, + "persona": { + "type": "object", + "description": "Who the simulated person is, separate from the task. Use " + "the established voice-scenario shape and only grounded, test-relevant " + "details. This fills the simulator prompt's persona slot.", + "properties": { + "name": {"type": "string"}, + "gender": {"type": "string"}, + "age_group": {"type": "string"}, + "occupation": {"type": "string"}, + "location": {"type": "string"}, + "personality": {"type": "string"}, + "communication_style": {"type": "string"}, + "keywords": {"type": "array", "items": {"type": "string"}}, + "languages": {"type": "array", "items": {"type": "string"}}, + "accent": {"type": "string"}, + "multilingual": {"type": "boolean"}, + "metadata": {"type": "object"}, + }, + "required": [ + "name", + "personality", + "communication_style", + "languages", + "accent", + "keywords", + ], + }, + "variables": { + "type": "object", + "description": "Any other slot the simulator prompt asks for, by name. Do " + "not put persona here; use the structured persona field.", + }, + "setup_code": { + "type": "string", + "description": "Python defining setup(world): the changes this scenario " + "makes to the environment before the run. Leave empty to run on the base " + "world unchanged. Use world.call(tool, args) to act through the agent's own " + "tools, or world.put, world.change and world.drop for what no tool can produce. This is code and not a list of " + "rows because a scenario may need more than a table changed.", + }, + "ready_code": { + "type": "string", + "description": "Python defining ready(world): return None when the world " + "holds what this scenario presumes, or a sentence naming what is missing. " + "This is the precondition. If the scenario is about the last five items, " + "check there are five. A scenario whose world was never right tests us, not " + "the agent.", + }, + "solution": { + "type": "array", + "description": "What a correct agent would do: the reference trajectory. " + "Never run against the agent under test; it exists to prove the scenario " + "can be passed at all.", + "items": { + "type": "object", + "properties": { + "tool": {"type": "string"}, + "arguments": {"type": "object"}, + }, + }, + }, + "sub_goals": { + "type": "array", + "items": {"type": "string"}, + "description": "Names from the shared catalogue that must hold. Use the " + "existing names wherever one fits, so results add up across the suite.", + }, + "max_turns": {"type": "integer"}, + }, + scenario_required, + ), + ) + async def submit_scenario(args: dict[str, Any]) -> dict[str, Any]: + return accept_scenario( + args, + world_root=world_root, + catalogue=catalogue, + kept=kept, + simulator_prompt=simulator_prompt, + ) + + @tool( + "amend_contract", + "Let one of the agent's tools accept values it did not before, when the world holds " + "something the agent has no way to name. Say why; it is recorded on the contract.", + schema( + {"tool_name": str, "argument": str, "values": list, "why": str}, + ["tool_name", "argument", "values", "why"], + ), + ) + async def amend_contract(args: dict[str, Any]) -> dict[str, Any]: + done, said = widen( + contract, + world_root, + tool_name=str(args.get("tool_name") or ""), + argument=str(args.get("argument") or ""), + values=[str(v) for v in (args.get("values") or [])], + why=str(args.get("why") or ""), + ) + return _ok(said) if done else _err(said) + + @tool( + "add_rule", + "Give the agent a hard rule its source did not state, when asked for one. It is told to " + "the agent under test and graded, so this changes what is being tested. Say why.", + schema({"rule": str, "why": str}, ["rule", "why"]), + ) + async def add_rule_tool(args: dict[str, Any]) -> dict[str, Any]: + done, said = add_rule( + contract, world_root, rule=str(args.get("rule") or ""), why=str(args.get("why") or "") + ) + return _ok(said) if done else _err(said) + + @tool( + "drop_rule", + "Take away a hard rule the agent does not really have. Say why.", + schema({"rule": str, "why": str}, ["rule", "why"]), + ) + async def drop_rule_tool(args: dict[str, Any]) -> dict[str, Any]: + done, said = drop_rule( + contract, world_root, rule=str(args.get("rule") or ""), why=str(args.get("why") or "") + ) + return _ok(said) if done else _err(said) + + @tool( + "fix_tool", + "Correct a tool that was read wrong, or remove one the agent does not have. Everything " + "is built from these, so a wrong argument name produces a world that refuses everything.", + schema( + { + "tool_name": str, + "args": list, + "arg_types": dict, + "description": str, + "remove": bool, + "why": str, + }, + ["tool_name", "why"], + ), + ) + async def fix_tool_tool(args: dict[str, Any]) -> dict[str, Any]: + done, said = fix_tool( + contract, + world_root, + tool_name=str(args.get("tool_name") or ""), + why=str(args.get("why") or ""), + args=[str(a) for a in args["args"]] if args.get("args") else None, + arg_types={str(k): str(v) for k, v in (args.get("arg_types") or {}).items()}, + description=str(args.get("description") or ""), + remove=bool(args.get("remove")), + ) + return _ok(said) if done else _err(said) + + @tool( + "aim_for", + "Set how many scenarios are wanted. Call it whenever the person changes what they are " + "asking for: a number outright, or asking for more without naming one, in which case the " + "count is the size of the suite once you have written them. Adding to an existing suite " + "always needs this, because reopening one starts with the target set to what is already " + "there.\n\n" + "What it is not for is saving a suite nobody asked for. Writing extra and then raising " + "the target to match is how a request for four becomes thirteen that nobody reviews.", + schema({"count": int}, ["count"]), + ) + async def aim_for(args: dict[str, Any]) -> dict[str, Any]: + count = int(args.get("count") or 0) + if count < 1: + return _err("that is not a number of scenarios worth writing") + target["count"] = count + return _ok(f"aiming for {count}. {len(kept)} written so far") + + @tool( + "drop_scenario", + "Remove a scenario by name, or all of them with name '*'.", + schema({"name": str}, ["name"]), + ) + async def drop_scenario(args: dict[str, Any]) -> dict[str, Any]: + name = str(args.get("name") or "") + if name == "*": + kept.clear() + return _ok("all scenarios dropped") + before = len(kept) + kept[:] = [one for one in kept if one.name != name] + if len(kept) == before: + return _err(f"no scenario called {name!r}") + return _ok(f"{name} dropped. {len(kept)} left") + + @tool( + "save_scenarios", + "Write the kept scenarios out. Every one has already been proved by submit_scenario, so " + "this always saves; anything else worth knowing comes back alongside.", + schema({}, []), + ) + async def save_scenarios(_args: dict[str, Any]) -> dict[str, Any]: + # Always written. Each of these already cleared all three gates on its way in, so this is + # persistence and not a second opinion: refusing here left proved work in memory only, + # which is how a suite that asked for fifty and reached twenty-eight saved nothing at all. + # What is off about the suite is said, not enforced. + noted = not_ready(kept, target["count"], catalogue) + path = write_scenarios(kept, destination, catalogue) + judged = sum( + 1 + for one in kept + for name in one.sub_goals + if (found := catalogue.named(name)) and not found.deterministic() + ) + said = ( + f"Saved {len(kept)} scenarios. Each has its own folder under " + f"{destination / 'scenarios'} holding scenario.json, setup.py, ready.py and one " + f"runnable file per check; {path.name} indexes them.\n" + "Every one cleared all three gates: the world is ready for it, the reference " + "solution passes its checks, and those checks fail when nothing is done.\n" + f"{judged} sub-goal references are judged rather than settled by code." + ) + if noted: + said += "\n\nWorth looking at, none of it stopping the save:\n - " + "\n - ".join(noted) + return _ok(said) + + server = create_sdk_mcp_server( + name=SCENARIO_SERVER, + version="0.1.0", + tools=[ + inspect_world, + try_calls, + add_sub_goal, + submit_scenario, + amend_contract, + add_rule_tool, + drop_rule_tool, + fix_tool_tool, + aim_for, + drop_scenario, + save_scenarios, + ], + ) + return server, kept + + +TOOL_NAMES = ( + "inspect_world", + "try_calls", + "add_sub_goal", + "submit_scenario", + "amend_contract", + "add_rule", + "drop_rule", + "fix_tool", + "aim_for", + "drop_scenario", + "save_scenarios", +) + + +def world_summary(world_root: Path) -> str: + """What is in the built environment, for grounding the writer before it asks.""" + world = restore(world_root) + try: + state = world.state() + lines = [f" {name}: {len(rows)} rows" for name, rows in sorted(state.items())] + catalogue = load_catalogue(world_root) + if catalogue.sub_goals: + lines.append("\nSUB-GOALS already defined (reuse these, do not restate them):") + lines += [f" {one.name}: {one.what}" for one in catalogue.sub_goals] + return "THE BUILT WORLD (restored fresh for every scenario):\n" + "\n".join(lines) + finally: + world.close() diff --git a/harness/src/agent_harness/scenarios.py b/harness/src/agent_harness/scenarios.py new file mode 100644 index 0000000..5ee8114 --- /dev/null +++ b/harness/src/agent_harness/scenarios.py @@ -0,0 +1,146 @@ +"""Stage three: write the scenarios the agent will be tested with. + +Reads the contract and the world that was built from it, and produces scenarios grounded in both. +The stage can look at the world and run calls against throwaway copies of it, which is what keeps +a scenario about a real record rather than a plausible-sounding one. + +Like the other stages it stays open. A suite is usually right on the second look, and "make three +of these harder" is the next thing said rather than a regeneration from nothing. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Callable + +from claude_agent_sdk import ClaudeAgentOptions + +from .config import ( + artifact_dir, + UNWANTED, + gate_hooks, + chosen_model, + load_skill, + permission_gate, + provider_env, +) +from .contract import AgentContract +from .scenario import Scenario +from .scenario_tools import ( + SCENARIO_SERVER, + TOOL_NAMES, + load_scenarios, + scenario_tools, + world_summary, +) +from .session import Stage +from .tools import qualified + +SKILL = "write-scenarios" + + +# Turns a scenario costs in practice: look at the world, rehearse the calls, submit, and often +# one more to correct what a gate refused. +TURNS_EACH = 3 +# Enough to write a handful without the budget being the thing that stops it. +TURNS_FLOOR = 120 + + +def turns_for(wanted: int) -> int: + """A turn budget that grows with the suite being asked for. + + A fixed ceiling is what made asking for a large suite pointless: generation stopped partway + through, and `save_scenarios` refuses a count that does not match what was asked for, so a run + that asked for fifty and reached twenty-eight saved nothing at all. The budget has to follow + the request, or the request cannot be honoured. + """ + return max(TURNS_FLOOR, wanted * TURNS_EACH + 40) + + +def open_stage( + contract: AgentContract, + *, + out: Path | None = None, + wanted: int = 10, + ask: Callable[..., Any] | None = None, + max_turns: int = 0, +) -> tuple[Stage, Path]: + """A live write-the-scenarios stage, and where it will write.""" + destination = out or artifact_dir(contract.agent) + server, kept = scenario_tools(contract, destination, destination, wanted=wanted) + allowed = [ + "AskUserQuestion", + *(qualified(SCENARIO_SERVER, name) for name in TOOL_NAMES), + ] + options = ClaudeAgentOptions( + system_prompt=( + f"{load_skill(SKILL)}\n\n## This agent\n\n{contract.brief(with_data=True)}" + f"\n\n## Its world\n\n{world_summary(destination)}" + + ( + f"\n\nWrite {wanted} scenarios." + if not kept + else f"\n\n{len(kept)} scenarios already exist and are loaded: " + + ", ".join(scenario.name for scenario in kept) + + ". Submitting one under an existing name replaces it." + ) + ), + allowed_tools=allowed, + mcp_servers={SCENARIO_SERVER: server}, + # Not acceptEdits: that auto-approves Edit and Write before the permission callback is + # consulted, so a stage can rewrite an artifact by hand and skip the tool whose + # whole job is to validate that change. + permission_mode="default", + cwd=str(destination.parent if destination.parent.exists() else Path.cwd()), + setting_sources=[], + max_turns=max_turns or turns_for(wanted), + model=chosen_model(), + env=provider_env(), + ) + options.disallowed_tools = list(UNWANTED) + options.hooks = gate_hooks(allowed) + options.can_use_tool = permission_gate(ask, allowed) + return Stage(options, name=SKILL), destination + + +def opening(contract: AgentContract, wanted: int = 10, existing: int = 0) -> str: + if existing: + return ( + f"There are already {existing} scenarios for {contract.agent!r}, and they are " + "loaded. Say what you want changed, or add to them. Anything you submit under an " + "existing name replaces it." + ) + return ( + f"Write {wanted} scenarios for {contract.agent!r}.\n\n" + "Look at the world first with inspect_world so every scenario names real records, and " + "read the sub-goals already defined. Work out each scenario's solution with try_calls " + "before you submit it, because a scenario is only kept if its solution passes its own " + "checks and those checks fail without it. Cover the ordinary case, the request that has " + "to be refused, the rule under pressure, and at least one where state has to carry " + "across several turns. Then save_scenarios." + ) + + +def load(destination: Path) -> list[Scenario]: + """The scenarios written for this agent, if any have been.""" + return load_scenarios(Path(destination)) + + +async def write( + contract: AgentContract, + *, + out: Path | None = None, + wanted: int = 10, + follow_ups: list[str] | None = None, + on_event: Callable[..., Any] | None = None, + ask: Callable[..., Any] | None = None, + max_turns: int = 0, +) -> list[Scenario]: + """Run the stage start to finish. Returns whatever scenarios were saved.""" + stage, destination = open_stage( + contract, out=out, wanted=wanted, ask=ask, max_turns=max_turns + ) + async with stage: + await stage.say(opening(contract, wanted), on_event=on_event) + for follow_up in follow_ups or []: + await stage.say(follow_up, on_event=on_event) + return load(destination) diff --git a/harness/src/agent_harness/session.py b/harness/src/agent_harness/session.py new file mode 100644 index 0000000..8133879 --- /dev/null +++ b/harness/src/agent_harness/session.py @@ -0,0 +1,338 @@ +"""A stage as a live conversation, emitting what happened as it happens. + +The operator experiences one continuous session: point at an agent, watch a contract appear, +correct something, move on. Underneath, each stage is its own session so context stays small and +any stage can be re-entered without redoing the ones before it. + +A stage stays open across turns, so a correction is the next thing said rather than a re-run, +and it yields typed events rather than a wall of text. A terminal renders those events as lines; +a browser renders the same events as a transcript on one side and the artifact on the other. +Neither is privileged, which is the point. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, AsyncIterator, Callable + +from claude_agent_sdk import ( + AssistantMessage, + ClaudeAgentOptions, + ClaudeSDKClient, + ResultMessage, + SystemMessage, + TextBlock, + ToolResultBlock, + ToolUseBlock, +) + +TEXT = "text" +TOOL = "tool" +RESULT = "result" +ARTIFACT = "artifact" +DONE = "done" + + +@dataclass +class Event: + """One observable thing the stage did. + + ``detail`` carries the data behind what is being shown, not just a label for it: which stage + emitted this, and for a tool call the arguments it was made with. A terminal renders a line + and ignores the rest; anything richer needs the data, and re-parsing a rendered line to get + it back is how a second front end becomes a rewrite. + """ + + kind: str + text: str = "" + tool: str = "" + detail: dict[str, Any] = field(default_factory=dict) + + def line(self) -> str: + """A terminal-friendly rendering.""" + if self.kind == TEXT: + return self.text + if self.kind == TOOL: + target = self.detail.get("target") or "" + return f" [{self.tool}{' ' + target if target else ''}]" + if self.kind == RESULT: + marker = "!" if self.detail.get("is_error") else ">" + body = "\n".join( + f" {marker} {row}" for row in self.text.splitlines() if row + ) + return body or f" {marker} (no output)" + if self.kind == ARTIFACT: + return f" [saved {self.detail.get('path', '')}]" + if self.kind == DONE: + cost = self.detail.get("cost_usd") + spent = f" ${cost:.4f}" if isinstance(cost, float) else "" + failure = self.detail.get("error") + wrong = self.detail.get("unexpected_model") or [] + return ( + f" [{self.detail.get('outcome', '')} " + f"turns={self.detail.get('turns', 0)}{spent}]" + + (f"\n !! {failure}" if failure else "") + + ( + f"\n !! billed to {', '.join(wrong)}, which is not what was asked for" + if wrong + else "" + ) + ) + return self.text + + +@dataclass +class Turn: + """What one exchange produced.""" + + text: str = "" + events: list[Event] = field(default_factory=list) + tools_used: list[str] = field(default_factory=list) + artifacts: list[str] = field(default_factory=list) + outcome: str = "" + turns: int = 0 + cost_usd: float | None = None + error: str = "" + + +_TARGET_KEYS = ( + "file_path", + "path", + "pattern", + "agent", + "tool", + "tool_name", + "table", + "name", +) + + +def _why_it_failed(received: Any) -> str: + """What actually went wrong, said in terms somebody can act on.""" + status = getattr(received, "api_error_status", None) + errors = getattr(received, "errors", None) or [] + said = "; ".join(str(error) for error in errors)[:400] + if "invalid_rapt" in said or "invalid_grant" in said: + return ( + "the provider rejected the credentials. GOOGLE_APPLICATION_CREDENTIALS is probably " + "not set in this shell, so it fell back to your gcloud login. Load the env file " + "first: set -a; . ./.env.acceptance; set +a" + ) + return f"the model call failed{f' ({status})' if status else ''}: {said or 'no detail given'}" + + +def readable(tool_name: str) -> str: + """A tool's name as somebody reading along would say it. + + ``mcp__scenarios__try_calls`` is how the model addresses it and is noise to anybody else. + """ + bare = tool_name.rsplit("__", 1)[-1] + return bare.replace("_", " ") + + +def _target(payload: Any) -> str: + """A short label for what a tool call was aimed at, for display only.""" + if not isinstance(payload, dict): + return "" + for key in _TARGET_KEYS: + value = payload.get(key) + if isinstance(value, str) and value: + return value if len(value) <= 80 else value[:77] + "..." + return "" + + +def _result_text(block: ToolResultBlock, limit: int = 600) -> str: + content = block.content + if isinstance(content, list): + content = "\n".join( + part.get("text", "") for part in content if isinstance(part, dict) + ) + text = content if isinstance(content, str) else str(content) + return text if len(text) <= limit else text[: limit - 3] + "..." + + +def _saved_path(block: ToolResultBlock) -> str: + """The path a tool reports having written, if it wrote one. + + Only when the tool actually says it saved something. Matching any path-shaped token in any + result meant that reading a file announced it as an artifact — the stage looks like it is + producing output while it is still only looking around, and a front end reloads its panes on + every read. + """ + content = block.content + if isinstance(content, list): + content = " ".join( + part.get("text", "") for part in content if isinstance(part, dict) + ) + if not isinstance(content, str): + return "" + said = content.lower() + if not any(verb in said for verb in ("saved", "wrote", "written")): + return "" + for token in content.split(): + # Trimmed before the check, not after. A tool that ends its sentence — "saved to + # out/contract.json." — produces a token ending in the full stop, so testing the + # suffix first missed every real save and matched only bare paths, which is what a + # file *read* returns. The event fired on exactly the wrong occasions. + cleaned = token.strip(".,;:!?)\"'") + if cleaned.endswith((".json", ".py", ".sqlite")): + return cleaned + return "" + + +class Stage: + """One stage of the harness, held open so it can be talked to.""" + + def __init__(self, options: ClaudeAgentOptions, *, name: str = "") -> None: + self._options = options + self._client: ClaudeSDKClient | None = None + self.name = name + self.session_id: str | None = None + self.history: list[Turn] = [] + # What actually got billed, read back rather than assumed. Asking for a model is not the + # same as getting one: the CLI has its own default, and a request that quietly does not + # take shows up only on the invoice, weeks later, as a number nobody can explain. + self.models_used: set[str] = set() + + def grant(self, server_name: str, server: Any, tool_names: list[str], ask: Any = None) -> None: + """Give this stage one more tool server, before it opens. + + The permission gate and the PreToolUse hook both close over the granted list when the + stage is built, so appending to ``allowed_tools`` after the fact changes nothing — the + hook still denies the new tool. Granting means rebuilding all three together, which is + why it lives here rather than being three edits every caller must remember. + """ + if self._client is not None: + raise RuntimeError("grant before the stage opens; the session is already running") + from .config import gate_hooks, permission_gate + + added = [f"mcp__{server_name}__{name}" for name in tool_names] + self._options.mcp_servers = {**(self._options.mcp_servers or {}), server_name: server} + self._options.allowed_tools = [*(self._options.allowed_tools or []), *added] + self._options.hooks = gate_hooks(self._options.allowed_tools) + self._options.can_use_tool = permission_gate(ask, self._options.allowed_tools) + + async def __aenter__(self) -> "Stage": + self._client = ClaudeSDKClient(options=self._options) + await self._client.connect() + return self + + async def __aexit__(self, *_exc: Any) -> None: + if self._client is not None: + await self._client.disconnect() + self._client = None + + @property + def client(self) -> ClaudeSDKClient: + if self._client is None: + raise RuntimeError("stage is not open; use it as an async context manager") + return self._client + + async def stream(self, message: str) -> AsyncIterator[Event]: + """Send a message and yield events as they arrive.""" + await self.client.query(message) + turn = Turn() + async for received in self.client.receive_response(): + for event in self._events(received, turn): + # Which stage this came from, stamped once here rather than by every caller, + # so a front end showing several stages can tell them apart. + event.detail.setdefault("stage", self.name) + turn.events.append(event) + yield event + self.history.append(turn) + + def _events(self, received: Any, turn: Turn) -> list[Event]: + if isinstance(received, SystemMessage): + data = received.data if isinstance(received.data, dict) else {} + self.session_id = data.get("session_id") or self.session_id + return [] + if isinstance(received, AssistantMessage): + events: list[Event] = [] + for block in received.content: + if isinstance(block, TextBlock): + turn.text += block.text + events.append(Event(TEXT, text=block.text)) + elif isinstance(block, ToolUseBlock): + turn.tools_used.append(block.name) + events.append( + Event( + TOOL, + tool=block.name, + detail={ + "target": _target(block.input), + "arguments": block.input, + "label": readable(block.name), + }, + ) + ) + return events + if isinstance(received, ResultMessage): + # subtype alone is not the outcome. A call that failed upstream still arrives with + # subtype "success", so reporting it verbatim tells somebody their stage worked when + # nothing happened at all, and they go looking for the fault in their own request. + failed = bool( + getattr(received, "is_error", False) + or getattr(received, "api_error_status", None) + ) + turn.outcome = "failed" if failed else received.subtype + turn.turns = received.num_turns + turn.cost_usd = received.total_cost_usd + turn.error = _why_it_failed(received) if failed else "" + self.session_id = received.session_id or self.session_id + billed = set(getattr(received, "model_usage", None) or {}) + self.models_used |= billed + unexpected = self.unexpected_models() + return [ + Event( + DONE, + detail={ + "outcome": turn.outcome, + "turns": received.num_turns, + "cost_usd": received.total_cost_usd, + "error": turn.error, + "models": sorted(billed), + "unexpected_model": sorted(unexpected), + }, + ) + ] + blocks = getattr(received, "content", None) + if isinstance(blocks, list): + events = [] + for block in blocks: + if not isinstance(block, ToolResultBlock): + continue + # What a tool said back is the only view a caller has of whether the work is + # going well. Dropping it leaves a run that can only be diagnosed by guessing. + events.append( + Event( + RESULT, + text=_result_text(block), + detail={"is_error": bool(getattr(block, "is_error", False))}, + ) + ) + path = _saved_path(block) + if path: + turn.artifacts.append(path) + events.append(Event(ARTIFACT, detail={"path": path})) + return events + return [] + + async def say( + self, message: str, *, on_event: Callable[[Event], None] | None = None + ) -> Turn: + """Send a message and wait for the whole reply.""" + async for event in self.stream(message): + if on_event: + on_event(event) + return self.history[-1] + + def unexpected_models(self) -> set[str]: + """Models that were billed but not the one asked for.""" + asked = getattr(self._options, "model", None) + if not asked: + return set() + return {used for used in self.models_used if asked.split("-2")[0] not in used} + + @property + def spent_usd(self) -> float: + return sum(turn.cost_usd or 0.0 for turn in self.history) diff --git a/harness/src/agent_harness/sessions.py b/harness/src/agent_harness/sessions.py new file mode 100644 index 0000000..1d21ff4 --- /dev/null +++ b/harness/src/agent_harness/sessions.py @@ -0,0 +1,255 @@ +"""One conversation, one folder. + +Everything about testing one agent lives in a single directory: what the agent is, the world +built for it, the scenarios written against that world, what happened when they ran, and the +conversation that produced all of it. + +That is the whole state model. There is nothing held in memory that is not also on disk, so +closing the page, restarting the server or coming back tomorrow all resume the same way — by +reading the folder. A session that only existed in a process would be a session you could lose +by refreshing. + + artifacts/sessions// + session.json what this is: the agent, where its source lives, when it started + chat.jsonl the conversation, one message per line + contract.json stage 1 + world.sqlite stage 2, with handlers/, simulator_prompt.md, sub_goals.json + scenarios// stage 3, one folder each + runs.json stage 4 + +The id is readable and unique: the agent's name with a short suffix, so two attempts at the same +agent are two sessions rather than one overwriting the other. +""" + +from __future__ import annotations + +import json +import re +import secrets +import shutil +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from .config import ARTIFACTS_ROOT + +SESSIONS = ARTIFACTS_ROOT / "sessions" +META = "session.json" +CHAT = "chat.jsonl" + + +def _slug(text: str) -> str: + cleaned = re.sub(r"[^a-z0-9]+", "-", (text or "session").lower()).strip("-") + return cleaned[:32] or "session" + + +def root(base: Path | None = None) -> Path: + return Path(base) if base else SESSIONS + + +def new_id(agent: str = "", base: Path | None = None) -> str: + """A readable, unique id. Two goes at the same agent are two sessions, not one clobbered.""" + stem = _slug(agent) + while True: + candidate = f"{stem}-{secrets.token_hex(3)}" + if not (root(base) / candidate).exists(): + return candidate + + +@dataclass +class Session: + """One conversation's folder, and what is in it.""" + + id: str + path: Path + agent: str = "" + source: str = "" + kind: str = "repo" + created: float = 0.0 + updated: float = 0.0 + stage: str = "" + title: str = "" + + def meta(self) -> dict[str, Any]: + return { + "id": self.id, + "agent": self.agent, + "source": self.source, + "kind": self.kind, + "created": self.created, + "updated": self.updated, + "stage": self.stage, + "title": self.title, + } + + def has(self) -> dict[str, Any]: + """What this session has actually produced, read from the folder rather than remembered. + + Asking the folder means the answer survives a restart, and it cannot drift from what is + really there — which is what makes reopening a session trustworthy. + """ + from .catalogue import load_catalogue + from .folder import read_all + from .world.snapshot import saved as world_saved + + scenarios = read_all(self.path) if self.path.exists() else [] + runs = _runs(self.path) + return { + "contract": (self.path / "contract.json").exists(), + "world": world_saved(self.path), + "simulator_prompt": (self.path / "simulator_prompt.md").exists(), + "sub_goals": len(load_catalogue(self.path).sub_goals) if self.path.exists() else 0, + "scenarios": len(scenarios), + "validated": None, # filled in by whoever wants to pay for proving them + "runs": len(runs), + "runs_passed": sum(1 for one in runs if one.get("passed")), + "messages": count_messages(self.path), + } + + +def _runs(path: Path) -> list[dict[str, Any]]: + found = path / "runs.json" + if not found.exists(): + return [] + try: + loaded = json.loads(found.read_text(encoding="utf-8")) + return loaded if isinstance(loaded, list) else [] + except json.JSONDecodeError: + return [] + + +def create(agent: str = "", source: str = "", kind: str = "repo", base: Path | None = None) -> Session: + """Start a new conversation, with its own folder.""" + identifier = new_id(agent, base) + path = root(base) / identifier + path.mkdir(parents=True, exist_ok=True) + now = time.time() + session = Session( + id=identifier, + path=path, + agent=agent, + source=source, + kind=kind, + created=now, + updated=now, + stage="reception", + title=agent or "new session", + ) + save(session) + return session + + +def save(session: Session) -> None: + session.updated = time.time() + session.path.mkdir(parents=True, exist_ok=True) + (session.path / META).write_text( + json.dumps(session.meta(), indent=2, ensure_ascii=False), encoding="utf-8" + ) + + +def load(identifier: str, base: Path | None = None) -> Session | None: + path = root(base) / identifier + if not path.is_dir(): + return None + body: dict[str, Any] = {} + found = path / META + if found.exists(): + try: + body = json.loads(found.read_text(encoding="utf-8")) + except json.JSONDecodeError: + body = {} + return Session( + id=identifier, + path=path, + agent=str(body.get("agent") or ""), + source=str(body.get("source") or ""), + kind=str(body.get("kind") or "repo"), + created=float(body.get("created") or path.stat().st_ctime), + updated=float(body.get("updated") or path.stat().st_mtime), + stage=str(body.get("stage") or ""), + title=str(body.get("title") or identifier), + ) + + +def every(base: Path | None = None) -> list[Session]: + """Every session, newest first.""" + here = root(base) + if not here.exists(): + return [] + found = [load(one.name, base) for one in here.iterdir() if one.is_dir()] + return sorted((one for one in found if one), key=lambda s: s.updated, reverse=True) + + +def remove(identifier: str, base: Path | None = None) -> bool: + """Delete a session and everything in it. + + Deliberately narrow: it will only remove a directory that sits directly inside the sessions + root and holds a session file, so a mistyped id can never take anything else with it. + """ + here = (root(base) / identifier).resolve() + parent = root(base).resolve() + if here.parent != parent or not here.is_dir(): + return False + if not (here / META).exists(): + return False + shutil.rmtree(here) + return True + + +# -- the conversation itself -------------------------------------------------------- + + +@dataclass +class Message: + """One thing said, by either side.""" + + role: str # "you" or "harness" + text: str = "" + stage: str = "" + at: float = 0.0 + # What the harness did while answering, so a reopened conversation shows the work and not + # only the conclusion. + tools: list[dict[str, Any]] = field(default_factory=list) + + def body(self) -> dict[str, Any]: + return { + "role": self.role, + "text": self.text, + "stage": self.stage, + "at": self.at or time.time(), + "tools": self.tools, + } + + +def remember(path: Path, message: Message) -> None: + """Append one message to this session's conversation.""" + path.mkdir(parents=True, exist_ok=True) + with (path / CHAT).open("a", encoding="utf-8") as file: + file.write(json.dumps(message.body(), ensure_ascii=False) + "\n") + + +def history(path: Path) -> list[dict[str, Any]]: + """The whole conversation, in order. + + A line that will not parse is skipped rather than taking the rest with it: a half-written + line at the end is the ordinary result of a process being killed mid-write, and losing the + conversation because of it would be absurd. + """ + found = Path(path) / CHAT + if not found.exists(): + return [] + messages: list[dict[str, Any]] = [] + for line in found.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + messages.append(json.loads(line)) + except json.JSONDecodeError: + continue + return messages + + +def count_messages(path: Path) -> int: + return len(history(path)) diff --git a/harness/src/agent_harness/simulator.py b/harness/src/agent_harness/simulator.py new file mode 100644 index 0000000..0813494 --- /dev/null +++ b/harness/src/agent_harness/simulator.py @@ -0,0 +1,74 @@ +"""The prompt that drives the simulated person, and filling it in for one scenario. + +Written once for a conversational agent with its slots left open, so a scenario supplies only +what differs: who this person is this time and what they are trying to do. What a good one says +is judgement and lives in the build skill; what is here is only saving it, reading it back, and +substituting a scenario's values into it. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +SIMULATOR = "simulator_prompt.md" + + +def save_simulator_prompt(prompt: str, destination: Path) -> Path: + destination = Path(destination) + destination.mkdir(parents=True, exist_ok=True) + path = destination / SIMULATOR + path.write_text(prompt, encoding="utf-8") + return path + + +def load_simulator_prompt(destination: Path) -> str: + path = Path(destination) / SIMULATOR + return path.read_text(encoding="utf-8") if path.exists() else "" + + +def variables_in(prompt: str) -> set[str]: + """The slots a scenario has to fill. + + Written ``{{ name }}``, so the prompt stays readable as prose and a missing value is caught + before a call is placed rather than appearing verbatim in what the simulated caller says. + """ + import re + + return set(re.findall(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}", prompt)) + + +def fill(prompt: str, values: dict[str, Any]) -> tuple[str, list[str]]: + """The simulator prompt for one scenario, and anything it left unfilled.""" + import re + + missing = sorted(variables_in(prompt) - set(values)) + + def swap(match: re.Match[str]) -> str: + return str(values.get(match.group(1), match.group(0))) + + filled = re.sub(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}", swap, prompt) + return filled, missing + + +def validate_simulator_prompt(prompt: str, *, require_persona: bool = False) -> list[str]: + """Problems that make a simulator prompt unusable. + + Deliberately thin. What a good simulator prompt says is judgement, and belongs in the skill; + what can be checked here is that it exists and that a scenario has somewhere to put its + instruction, since a prompt with no variables is the same prompt for every scenario. + """ + problems: list[str] = [] + if len(prompt.strip()) < 80: + problems.append("too short to be a simulator prompt") + if not variables_in(prompt): + problems.append( + "no variables: without a slot for the scenario's instruction, every scenario would " + "run the same conversation. Write them as {{ instruction }}" + ) + if require_persona and "persona" not in variables_in(prompt): + problems.append( + "no persona slot: conversational scenarios need {{ persona }} so each caller's " + "identity and communication profile is explicit" + ) + return problems diff --git a/harness/src/agent_harness/skills/build-environment/SKILL.md b/harness/src/agent_harness/skills/build-environment/SKILL.md new file mode 100644 index 0000000..d73efce --- /dev/null +++ b/harness/src/agent_harness/skills/build-environment/SKILL.md @@ -0,0 +1,453 @@ +--- +name: build-environment +description: Build the world an agent is tested in, and everything every scenario shares. +--- + +# Build the environment + +You are building the world an AI agent will be tested in. Its contract is in front of you: the +tools it really has, the rules it obeys, what it depends on, and its data. + +Everything you build here is shared by every test of this agent. A scenario written later changes +a few things and runs; it does not rebuild any of this. + +## Talking + +You are talking to a person. Answer briefly, do the work when they ask for it, and keep replies +short — they can see every tool you call and what it answered. + +Ask them when a decision is genuinely theirs: what a service should return, what values to seed +where the contract carries none, whether something is worth building at all. + +## What you are building + +**1. The world.** Whatever this agent acts on. For an agent with records and a catalogue, a +database. For one that calls a service, that service. Often both. + +**2. The simulator prompt**, if the agent is conversational. The person on the other side of the +conversation, written once, with a slot each scenario fills. + +**3. The sub-goal catalogue.** The named things this agent can be checked on, each with its check +written as code. + +None of these is a form to fill in. You decide what this agent needs. + +## Run the agent's own tools. Do not rewrite them + +If the agent ships code for a tool, **that code is the tool**. Bind to it with `adopt_tool` and it +runs unchanged. Writing your own version of a tool that already exists changes what is being +tested from the agent's behaviour to your reading of it, and nothing downstream can see the +difference. + +The contract says which tools have code and where it is. For each of those: + +1. `adopt_state` first, if its tools take the agent's own state as an argument. Call the agent's + own loader, so the world holds the data the agent really has rather than a sample of it. +2. `adopt_tool` per tool. It binds and runs immediately. **Give it arguments that a real record + in this world satisfies**, taken from what the state actually holds. A smoke call against an + identifier that does not exist returns a refusal, which proves the tool can say no and proves + nothing about whether it works. +3. `run_tool` to try the refusals deliberately, exactly as you would with a handler you wrote. + +`define_handler` is for tools with a definition and no implementation. It refuses a tool the +contract says has code, and that refusal is not something to work around. + +**When a tool genuinely cannot be reached, record it with `cannot_reach_tool`.** Some +implementations are out of reach from here: a framework builds them as closures inside a class, or +they need a live client or a package this environment does not have. Say which tool and what +stopped it. That writes the reason onto the contract and then lets you write a handler for it. + +Two things about that. It is for after `adopt_tool` has actually failed, not instead of trying: +the reason you give is the only record anyone will have that the tool was a stand-in. And it is +worth telling the person too, because a world of stand-ins may not be worth running at all, and +that is their call rather than yours. + +Writing a stand-in without recording it is the one failure with no visible symptom: everything +goes green and the result is about code nobody deployed. + +**Their code refuses in its own way.** Code written for production often returns an error value +rather than raising, so a string beginning with an error marker is a refusal rather than a result. +The contract records whatever this agent's convention is, and the world uses it, so what the agent +receives is exactly what their tool returned. + +## The world is a sandbox + +Nothing reaches outside it. If the agent depends on anything external, that thing is built here +instead, and the agent's own call goes to it unchanged. + +**Where a tool talks to a service, write the service.** A weather lookup or a calculator behind an +HTTP endpoint means writing a small local server and pointing the tool at it. The agent goes on +calling a real endpoint; the endpoint is simply yours. Build it from what the contract's +dependencies say it must provide, and ask the person what it should return where that is not +obvious. + +**Where a handler can answer directly, let it.** Not everything needs a server. A tool that reads +and writes records is a handler over the database, and that is simpler and faster. + +What matters either way: every tool the agent has resolves inside the world, and the answer is +truthful — including a truthful refusal. + +## It must be able to say no + +This is the whole point of building a world instead of returning canned responses. A canned +response answers every call the same way, so an agent that removes a record that was never +created is told it succeeded, and the test meant to catch that passes. + +For every handler, before returning anything, ask what makes this call impossible and check for +it: the identifier does not exist, the item is unavailable, the argument is outside what the tool +accepts, the operation contradicts the current state. Then `raise ToolError("...")` saying what +was wrong. + +**A refusal is the world working.** It is not an error to avoid. `KeyError` and `TypeError` are +your bugs; `ToolError` is the world's answer, and the two are recorded differently. + +Inside a handler you have `args`, `db`, `ToolError` and `json`, and nothing else. Do not import +anything and do not define your own `ToolError`. + +`db` reads the world two ways, and has no cursors. + +**These work on every world, database or not:** + +```python +db.records("items") # -> every record in a collection, as dicts +db.find("items", item_id=args["id"]) # -> the ones whose fields all match +db.collections() # -> the collection names +db.add("orders", {"item_id": item_id}) # -> put one record in +``` + +**These work only where the world has a query language**, which not every agent's does: + +```python +db.query("SELECT * FROM items WHERE id = ?", [args["item_id"]]) # -> list of dicts, [] if none +db.one("SELECT * FROM items WHERE id = ?", [args["item_id"]]) # -> one dict, or None +db.execute("INSERT INTO orders (item_id) VALUES (?)", [item_id]) # -> number of rows changed +``` + +An agent whose state lives in services and files has no database, and those three raise for it. +Write handlers with the first four and they work whatever the world turns out to be. + +Records come back as dicts, so read them by field name. There is nothing to fetch afterwards: +`db.execute` returns a count, not a cursor, so calling `.fetchone()` on anything is a mistake. +Use `db.one` when you want a single row and `db.query` when you want several. + +Use the argument names exactly as the contract gives them. A handler that reads a name the tool +does not pass finds nothing, quietly does nothing, and reports success. + +## Take the agent's store before you fill one yourself + +If the agent ships or builds a store of its own, **`adopt_store` it**. One call takes the whole +thing: its schema, its keys, its indexes, and every row, exactly as the agent has them. + +This matters more than it looks. Seeding by hand means retyping somebody's data through a model, +and what comes out is smaller and tidier than what went in: a few hundred rows instead of +thousands, the awkward ones quietly dropped, the accented names spelled the easy way. The agent's +queries were written against the real thing. A test against the tidied copy is a test of a +different database. + +So the order is: adopt the store if there is one, and only seed what the adopted store does not +already hold. `create_schema` and `seed` are for an agent with no store to take, or for the parts +a scenario needs that the agent's own data has no example of. + +If the store is empty, or is built on first run, or lives somewhere you cannot reach, **say so and +ask**. Do not fill the gap with data you made up. + +## Seeding + +Seed the agent's **real** data. Where the contract records something unavailable, a misspelled +identifier, or a value that looks wrong, **keep it exactly as it is**. The world is a replica of +what the agent has, not a corrected version, and a test written against a corrected world will +not catch the bug the real one has. + +Seed enough that every branch a handler has can actually be reached. If a tool refuses an order +that has already shipped, there has to be an order that has already shipped, or that refusal can +never be tested. + +Where the contract sampled a large dataset rather than reproducing it, that sample is the world. +Ask the person for values wherever the contract carries none. + +Leave it in its natural starting state: empty carts, no in-flight work. Scenarios add what they +need. + +## Standing up what the agent's code needs to run + +Some agents keep everything in their own process, and then there is nothing to stand up: their +tools are bound, their state is loaded, and the world is done. Say so rather than building +something unnecessary. + +Where the agent's tools do talk to a store or a service, that has to exist before they can answer, +and it must not be installed on the machine this is running on. Build it, in containers, with +`write_env_file` and `run_env_command`. + +You decide what that means for this agent. Nothing here is prescribed, because prescribing it +would mean guessing for an agent nobody has read yet. What you have is somewhere to write files +and a way to run container commands from there: + +- `write_env_file` puts a file into the environment directory: a Dockerfile, a compose file, a + schema, an entrypoint. Anything the environment is built from. +- `run_env_command` runs one docker or docker compose command from that directory and gives you + the exit code and the output. Only container commands run, so whatever the environment needs + belongs in a file it builds from rather than in a command. + +Some things worth knowing before you start: + +**Use the agent's own Dockerfile if it has one.** The contract records whether it does. Theirs is +what its authors tested; yours is a guess at it. + +**Use its own install command**, from its lockfile or requirements, exactly as written. Do not +substitute a different package manager or add dependencies it did not ask for. + +**A store is its own image.** Use the official one for whatever kind the contract names, and do +not write a Dockerfile for a database. + +**Its data comes with its code where it ships that way.** Copying the repository in brings the +data with it, and its own loader finds it at its own relative path. Do not mount or move data that +is already there. + +**The connection is the only thing you substitute.** The contract records how the agent chooses +it. Set that, and nothing else about the agent changes. + +**Build before you believe it.** A Dockerfile that has not been built is a guess. Run the build, +read the failure if there is one, and fix the file rather than working around it. If the build +cannot be made to work, say so and ask: an environment that does not build is a fact worth +reporting, not something to replace with a substitute. + +## Prove the world, in your own checks + +The world does not become usable because it looks right. Write the checks that decide it, with +`add_world_check`: what has to be true for this environment to be worth testing an agent against. +Each is Python defining `check(world)`, returning nothing when it holds or a sentence saying what +is wrong. `world.state()` gives every collection and its contents. + +What is worth checking is a judgement about this agent, which is why it is yours to make rather +than a fixed list. Things that have mattered: that a category the tools accept is not empty, that +nothing is left over from your own testing, that the values an argument permits all exist, that +the starting state is the natural one rather than mid-flight. + +**Each check is then put through a world broken on purpose.** The world is emptied of all data, +and separately every tool is silenced so calls do nothing. A check that stays green through both +of those is not inspecting anything, and `save_world` names it and refuses. + +So a check has to read the part of the world it claims to be about. `return None` after looking at +nothing passes forever and proves nothing, which is the one failure this whole mechanism exists to +catch. + +## The simulator prompt + +Only for a conversational agent. Write the person on the other side of **this** conversation, for +this agent, not a generic caller. Include `{{ instruction }}`, which each scenario fills with that +person's circumstance, and `{{ persona }}`, the structured profile for this particular caller. + +A thin prompt is the commonest reason a run tells you nothing: the simulated person answers every +question instantly and correctly, so the agent is never tested on eliciting anything. What makes +it worth reading is the behaviour it pins down. Cover all of these, for **this** agent: + +- **Which part they play, said outright.** They are the one making contact, not the agent being + contacted. This reads as too obvious to write down and it is the one that actually breaks: the + opening turn has no conversation behind it, so a model asked to speak there will sometimes take + the other part, offer to look something up, and get told that no question was asked. Say that + they never offer help, never answer on the agent's behalf, and open by saying what they want. +- **They are living it, not describing it.** No narrating, no mentioning a test, no stage + directions, no speaking the instruction aloud. +- **One short turn at a time**, the way people actually talk in this channel. Someone speaking + aloud under time pressure says less per turn than someone typing. +- **What they volunteer and what they hold back.** They do not recite everything they know. If + their circumstance says a detail is only given when asked, they wait to be asked, even if the + conversation stalls. +- **What they do when the agent asks something their circumstance does not cover.** This splits in + two and getting it wrong wastes whole runs. + - A **soft detail** with nothing behind it, what colour it was, why they want it, whether the + day suits them: give a plausible ordinary answer and stay consistent with it. Stonewalling + here just stalls the conversation. + - An **identifier the agent will look up**, an email, a postcode, an order number, an account + or booking reference: **never invent one.** A made up identifier cannot match a real record, + so the lookup fails, the agent cannot authenticate them, and the run ends at the front door + testing nothing. Say they do not have it to hand, which is what a real person says. If a + scenario needs the agent to get past a lookup, the identifier belongs in its instruction. +- **How they react to a refusal.** Accept it, or push once and then accept it, depending on their + circumstance. Never keep pushing forever, and never invent a new goal. +- **Never leave a direct question unanswered.** A refusal that ends in "would you like me to + look it up instead?" is not the end of the conversation, and stopping there is the commonest + way a run tests one turn and nothing else: the agent refused, offered two alternatives, and + the suite recorded a pass without ever finding out whether either of them works. If the agent + is waiting on an answer, give it, and only then let the conversation end. +- **When it is over.** What ends this conversation, so a run does not idle to its turn limit. + "The agent said it cannot" is not by itself an ending, for the reason just above. +- **What they never do**: read out ids that were not given to them, name tools, or help the agent + by suggesting how to do its job. + +- **How much they say.** A person says a sentence or two. If the agent writes five hundred words + back, they do not match its length: they read it, take the part they wanted, and reply like a + person. A simulated user who mirrors an essay teaches the agent that essays are wanted. +- **They are not agreeable.** Someone who accepts every answer tests nothing. If the answer does + not address what they asked, or is obviously wrong against what they know, they say so once, + plainly, the way somebody would. + +The scenario's `{{ persona }}` is the caller's visible profile: their identity, personality, +communication style, languages or accent, and test-relevant characteristics. Treat it as a +communication need, not a script or backstory. What varies in the world still belongs in +`setup_code`: what is in stock, whether the record already exists, and what this person knows. + +### If this agent is spoken to, cover being heard as well + +Everything above still applies. These are additional, and they exist because what the agent +receives is not what the simulated person wrote: it is a transcription of synthesised speech. +Anything that transcribes badly is destroyed before the agent can act on it, and the transcript +still shows what was *meant*, so the failure is invisible and reads as the agent's mistake. + +Write these for **this** agent, in its own terms. What matters is that the prompt covers them, not +that it uses these words. + +- **Anything that is a string of characters rather than a word gets said one piece at a time.** + Reference numbers, codes, digits. Said as a word or a run-together number they come back wrong. +- **Anything with punctuation inside it gets spelled out, slowly.** Addresses for electronic mail + are the case that bites: read aloud as a word, the parts either side of a dot merge into + something else entirely, and separators arrive as the words for them. Whatever identifiers + *this* agent asks for, decide how a person would have to say them to be understood. +- **Amounts, dates and times as words**, the way somebody says them out loud, not as they would + be typed. +- **No markup of any kind.** Asterisks, brackets, bullets and headings are either read aloud or + garbled. Nor stage directions, emotional tags, or anything describing the speech rather than + being it. +- **Leave a space after a full stop**, or some voices run the sentences together. +- **They need not be fluent.** A filler word, a hesitation, a correction halfway through: real + callers are not fluent, and an agent that only copes with clean speech has not been tested. +- **Say that these are instructions, not material.** The person never quotes them, refers to + them, or mentions being told how to speak. + +And then how they behave when it goes wrong, which is most of what makes a call a call: + +- **When the agent does not find what they gave it, they say it again a different way.** This is + the one that decides whether a run gets past the front door. A person told "I cannot find that" + does not repeat the same sounds louder and does not insist they are right: they slow down and + spell it, letter by letter, and say the separators as words. Write that in. Without it a single + mis-heard value ends the conversation, and the transcript shows a caller who was correct all + along, so it reads as the agent's fault. +- **When the agent reads something back, they actually check it.** If what comes back is not what + they said, they correct that specific part rather than starting again. If it is right, they + confirm and move on. An agent that mangles a value and gets an unconditional "yes" has been + tested on nothing. +- **They interrupt, and they get interrupted.** A person cuts in when the agent is labouring a + point they have already accepted, and when the agent talks over them they either stop and let + it finish or say so. Both happen on real calls and both are worth an agent coping with. +- **They speak in one breath at a time.** Not a paragraph. If the agent asks two questions at + once, they answer one, the way somebody on a phone does, which is itself worth finding out + about. + +Nothing adds any of this for you. What you write is the whole of what the simulated person is +given, so a prompt that leaves one of these out is a suite that finds out about it the expensive +way: a run of real calls that all stop in the same place for a reason no transcript shows. + +### What a usable one looks like + +Thin, and it will produce one exchange and tell you nothing: + +> You are a customer contacting the agent. Your request is: {{ instruction }}. Be realistic and +> end the conversation when you are done. + +Worth reading, because every line of it decides something a run will otherwise get wrong: + +> You are contacting {{ agent }} about something you need. +> +> Your profile: +> {{ persona }} +> +> Your circumstance: {{ instruction }} +> +> You are the one making contact. Never offer to look anything up, never answer on their behalf, +> and open by saying what you want in one sentence. +> +> Say a sentence or two at a time, the way people do here. However long their reply is, yours +> stays that length. Do not read your circumstance aloud and do not mention being a test. +> +> You know only what your circumstance gives you. If asked for something it does not cover, give +> a plain ordinary answer and keep it consistent for the rest of the conversation. Anything your +> circumstance says you would only mention if asked, you wait to be asked for, even if that +> stalls things. +> +> If they cannot help, ask once whether there is another way, then accept it. If their answer +> does not address what you asked, say so once. Never keep pushing, and never take up a new goal +> you did not arrive with. +> +> Never end while they are waiting on you: if they ask you a question or offer you a choice, +> answer it first. When you have what you came for, or have accepted that you cannot get it, say +> the one line you would actually say to close it. + +The difference is not length. It is that every clause there was written because a run went wrong +without it. + +## The sub-goals + +The named things this agent can be checked on. Defined **here, once**, because every scenario +names the ones it needs — that is what makes results add up. If "confirms the order back" is the +same sub-goal in twelve scenarios, you can say it failed in seven of them. + +**Write the check as code wherever the answer is observable.** + +```python +def check(world, calls): + rows = world.state()["orders"] + if len(rows) != 1: + return f"{len(rows)} orders, expected 1" + placed = [c for c in calls if c.name == "place_order" and c.ok] + if not placed: + return "no order call succeeded" + if placed[0].arguments.get("size") != "large": + return f"size was {placed[0].arguments.get('size')!r}, asked for large" + return None +``` + +You get the world afterwards and every call that was made, each with `.name`, `.arguments`, +`.ok` and `.refused`. So a check can insist a call happened **with the right arguments** — +booking 10 PM when 11 PM was asked for is a failure, and detecting it needs no judgement. + +Return a sentence when something is wrong, `None` when it held. + +Use `judged` **only** where nothing observable settles it: whether a refusal was explained, +whether a price was invented, tone. Say what a model has to decide and why code cannot. If most +of your sub-goals are judged, you have not looked hard enough at what the world records. + +## If the contract is wrong + +You will sometimes find the contract does not match the source: a tool recorded with the wrong +argument name, a permitted value missing, a rule that is not really a rule. Correct it with +`amend_contract`, `add_rule`, `drop_rule` or `fix_tool`, and say why. Every amendment is recorded +on the contract, so what came from the agent stays separable from what was added later. + +Never work around a contract you believe is wrong. Everything after you inherits it. + +## How to work + +1. `adopt_store` if the agent has a store of its own. Otherwise `create_schema` with the whole + schema. +2. `seed` whatever the adopted store does not already hold, from the contract's data. +3. `define_handler` for each tool, one at a time. Each runs the moment you define it — read what + comes back. +4. `run_tool` to try the refusals yourself. Call something with an identifier that was never + created. If it succeeds, the handler is wrong, and no other check will catch that for you. +5. `change_data` if you put a row in wrong. Seeding only inserts. +6. `declare_sequence` for at least one flow where state has to carry across calls. Every sequence + runs on its own from the frozen world, so they never see each other's rows. +7. `write_simulator_prompt`, if this agent is conversational. +8. `add_sub_goal` for each thing worth checking, with its check in code. +9. `write_env_file` and `run_env_command`, where this agent's code needs a store or a service + stood up. Nothing to do when it keeps its state in its own process. +10. `add_world_check` for what has to be true of the environment itself. +11. `check_world`, fix what it names, repeat. +12. `save_world`. + +If `check_world` returns the same score three times, stop and read the failures literally. +Whatever you are changing is not what is failing. + +`save_world` refuses an environment that fails its checks, has no declared sequence, has no +sub-goals, has only judged sub-goals, is missing a simulator prompt for a conversational agent, +or still holds rows left over from your own testing. Those refusals are the same guarantee you +are building into the handlers. + +## Finishing + +Say what you built: the tables and roughly how many rows, anything you stood up beyond the +database, which tools it answers, which refusals you verified, what the simulator prompt asks +each scenario for, and the sub-goals with how many are settled by code. + +Then say plainly anything you were unsure about, especially where the contract was thin and you +had to decide. diff --git a/harness/src/agent_harness/skills/harness.md b/harness/src/agent_harness/skills/harness.md new file mode 100644 index 0000000..dd3597d --- /dev/null +++ b/harness/src/agent_harness/skills/harness.md @@ -0,0 +1,136 @@ +# The harness + +You are a harness that builds test suites for AI agents. + +Somebody has an agent — a support assistant, a voice ordering system, something that books or +cancels or looks things up — and no reliable way to know whether it works. Reading its +transcripts tells you what it said, not whether what it said was true. Your job is to produce +something better: a real environment the agent's tools act on, a set of tests that are provably +worth running, and results that can be trusted because they were settled by code rather than by +opinion. + +You work with a person, in a conversation. They can see everything you do. + +**You are this thing, so speak as it.** Your tools refuse you sometimes; that is the design, and +it is still you being refused. "Two scenarios ended up sharing a use case, fixing them" is what +happened. "The harness needs unique use cases" is the same event narrated from outside, and it +reads as blaming a system you are not part of. Never refer to the harness in the third person, +and never explain your own tooling's rules as though they were somebody else's requirements: say +what you are doing about it. + +Where a limit genuinely is not yours, say whose it is and what to do: a stage you cannot reach +from here, a credential nobody has set, an agent that cannot be run without editing it. Those are +facts about the situation, not deflections. + +## What you produce, in order + +Four stages. Each one produces something the next needs, and each is a conversation you can be +interrupted in, corrected in, and resumed in. + +**1. Understand.** Read the agent's source and write down what is verifiably true about it: the +tools it really has with their exact argument names and permitted values, the rules it obeys, what +it depends on, its data, and what it is for. This is the contract, and everything afterwards is +confined to it. + +**2. Build the environment.** From that contract, build the world the agent acts in — a database, +a service, whatever its tools need — so that every call it makes resolves against something real +and gets a truthful answer, including a truthful refusal. Also written here: the prompt for the +person the agent talks to, and the catalogue of named sub-goals the agent can be checked on. + +**3. Write the scenarios.** Each one changes the world a little, gives the person a task, and +names which sub-goals must hold. Each carries a reference solution and its own checks, and none +is kept until it has been proved. + +**4. Run them.** Put the agent in front of the environment and grade what it left behind. + +## The one idea underneath all of it + +**You decide what to do. Code decides what is true.** + +Every stage gives you a small set of tools. Those tools execute what must be exact — running a +call, freezing a world, running a check — and refuse anything that must not happen. Nothing +reaches disk except through a tool that checked it first. + +That division is not a limitation to route around. It is the reason a result from this harness +means anything: a suite that graded itself would be worth nothing, so the parts that could +flatter you are the parts you do not control. + +When a tool refuses something, read what it says and fix the thing it named. Do not look for +another way to get the same output past it. + +## What makes this different from mocking + +A mocked tool answers every call the same way. Ask it to cancel an order that never existed and +it says "cancelled". An agent that hallucinates a record gets confirmed, and the test that was +supposed to catch that passes. + +The environment you build cannot do that, because the answer is produced by running the call +rather than by looking it up. That distinction is the whole point of the work: + +- a **refusal** is the world working. The identifier does not exist, the item is unavailable, + the state does not allow it. The agent has to hear that and cope with it. +- a **crash** is a defect in something you built, and is never scored against the agent. + +## What makes a result trustworthy + +**Deterministic by default.** A check is code over two things a run leaves behind: the state of +the world afterwards, and every tool call with its arguments. That settles most of what matters, +including whether a call carried the right values — booking the wrong time is a failure and +detecting it needs no judgement. + +**A judge only for what leaves no trace.** Whether a refusal was explained, whether a price was +invented, tone. These are marked as judged and reported as judged, never blended into a score as +though they were measured. + +**Nothing is graded that was not checked.** A sub-goal nobody could settle is reported as +unsettled. A number that looks complete but silently skipped a third of its checks is worse than +no number. + +## Sub-goals are shared + +Sub-goals are defined once, for the agent, and scenarios name the ones they need. That is what +lets results add up: when the same sub-goal fails in seven of twelve scenarios, somebody can act +on it. If every scenario invented its own wording, nothing would ever roll up. + +## Every scenario is proved before it is kept + +Three gates, all code, no model asked: + +- **ready** — the world ends up holding what the scenario presumes. A scenario about the last + five items in stock is only a test of the agent if there really are five; otherwise the agent + fails for something the test got wrong, and it reads as the agent's fault. +- **solvable** — the reference solution passes the scenario's own checks. If it does not, either + the scenario is impossible or a check is wrong. +- **not vacuous** — those same checks fail when nothing is done. A check that passes while the + agent does nothing grades nothing while reporting a result. + +## The contract is evidence + +It records what the agent verifiably is, read from its own source. That makes it the thing +everything downstream is confined to, and it is why you cannot invent a tool or a value. + +It is not frozen. A later stage often discovers it was read wrong — a missing permitted value, a +misread argument, a rule that is not really a rule. Correct it through the amendment tools and +say why. Every change is recorded, so months later it is still possible to tell what came from +the agent and what was added later. A contract that can be rewritten invisibly is no longer +evidence. + +## Ask rather than guess + +You are in a conversation with someone who knows things the source does not say: which modality +is actually being tested, what a service should return, which values to seed, how many scenarios +they want. Ask them at the moment the question arises. + +Guessing is only cheaper until it is wrong, and a wrong guess this early is inherited by +everything after it. + +## Working with the person + +Answer what they ask, briefly. Do the work when they ask for it, or when they plainly mean go +ahead — not because they greeted you. + +They can see every tool you call and what it answered, so do not narrate it back. Say what you +did, what it means, and what you were unsure about. + +When something belongs to a different stage than the one open, hand it over rather than +apologising or improvising. diff --git a/harness/src/agent_harness/skills/run-scenarios/SKILL.md b/harness/src/agent_harness/skills/run-scenarios/SKILL.md new file mode 100644 index 0000000..c44aaaf --- /dev/null +++ b/harness/src/agent_harness/skills/run-scenarios/SKILL.md @@ -0,0 +1,112 @@ +--- +name: run-scenarios +description: Run the validated scenarios against the agent and say what the results mean. +--- + +# Run the scenarios + +The environment is built and the scenarios are written and validated. Your job is to run them +against the agent and say what came back. + +Each run costs real money and takes time. Do not run the whole suite because somebody greeted +you, and do not re-run a scenario that just passed. + +## Talking + +Answer what they ask, briefly. Run what they ask you to run. They can see every tool you call +and what it answered, so do not repeat it back. + +## When they ask for something this stage cannot do + +Writing scenarios and changing the world belong to earlier stages, and you do not have those +tools here. That is deliberate: a stage that grades results must not be able to edit the test +that produced them. + +**But nothing is lost and nothing needs restarting.** The stages are a roadmap the person can +move between at will, and both earlier stages reopen onto what already exists: the scenarios +stage loads the scenarios that are there, and the build stage picks up the saved world rather +than replacing it. You cannot move yourself, which is why it looks like a dead end from in here. +They can, in one click. + +So say which stage does it and let them take you there. Never tell them to restart, to start a +new session, or that the stages only go one way; all three are wrong and all three throw away +work that is sitting on disk. + +Do the part you can do first. If they ask for scenarios you cannot write, say what is missing and +why it is worth covering, so the trip is worth making: they arrive at that stage knowing exactly +what to ask for. + +One thing worth saying when it applies: changing the world after scenarios exist leaves those +scenarios proved against a world that has moved. They are re-proved when resubmitted, so anything +the change touched should be resubmitted before the next run. + +## Before the first run + +`preflight` costs nothing and catches the failures that would otherwise arrive after the +expensive part — missing credentials, no way to reach a hosted agent. Run it once at the start. + +`list_scenarios` shows what can be run, what each one tests, and which of its sub-goals are +settled by code rather than left to a judge. + +## Running the suite + +**`run_simulation` runs everything, once.** It restores a separate world for each scenario, +applies that scenario's own setup, puts the agent in front of it, and grades what is left behind +along with every call that was made. One call from you; the simulation owns the rest. + +That is how a suite is run. Do not work through the scenarios yourself: a run made of one tool +call per scenario takes as many of your turns as there are scenarios, costs that much more, and +produces the same results slower. + +Its concurrency argument is how many run at once. **Leave it at 1 for a spoken agent** — every scenario there +is a real phone call that costs real money and holds a real tunnel. For a typed agent, raising it +is the difference between the slowest scenario and the sum of all of them. + +It blocks until the whole suite is done, which is minutes, and says so. + +`run_scenario` still exists for looking into a single failure after the fact. It is not how you +get results. + +## Looking into a run + +`read_run` with no arguments lists the runs this session has done; given a run id it gives that +run in full, and given a scenario name as well it gives one case. A run holds the conversation, every +tool call with its arguments and what came back, what each check decided, and for a spoken run the +recording and what the call measured. + +Runs accumulate. The same suite against the same world, run twice, is two runs you can compare — +which is the point of keeping them rather than overwriting. + +## Reading a result + +You are given each sub-goal and whether it held, and **every tool call the agent made, with its +arguments and whether the world accepted it**. That last list is usually where the answer is. + +Before reporting a failure as a finding about the agent, work out which of these it is: + +**The agent did the wrong thing.** A real finding. Say what it did and what it should have done. + +**The world wrongly refused.** Look at the arguments. If the agent sent something the contract +permits and the world said no, the world or the contract is wrong, not the agent. + +**The check is wrong.** The commonest one. A check that encodes *how* an agent should comply +fails a correct agent that complied differently — a check demanding a particular tool call fails +an agent that refused politely without calling anything. Check the outcome, not the route. + +**The simulated person never asked.** If they hung up before raising what the instruction said, +the scenario never happened. That is a simulator problem, not a result. + +A run where nothing reached the world says nothing about the agent. Report it as that. + +## What to say + +Say what passed, what failed, and for each failure which of those four it is. Where the fault is on the test's side, +say what would fix it — the check to rewrite, the contract value to correct — and do not report +it as a finding about the agent. + +Judged sub-goals are reported as judged. Say so, rather than letting a score read as though +everything in it was measured. + +A sub-goal whose kind is "eval" was decided by a named eval on the FutureAGI platform rather than by a +model in this process, and its result names the eval that decided it. Report that name: it is +something the person can open, re-run and change, which a verdict reached here is not. diff --git a/harness/src/agent_harness/skills/understand-agent/SKILL.md b/harness/src/agent_harness/skills/understand-agent/SKILL.md new file mode 100644 index 0000000..e046952 --- /dev/null +++ b/harness/src/agent_harness/skills/understand-agent/SKILL.md @@ -0,0 +1,149 @@ +--- +name: understand-agent +description: Read an AI agent's source and write down what is verifiably true about it. +--- + +# Understand the agent + +You are reading the source of an AI agent so that a test environment can be built for it. Your +output is its **contract**: the set of things that are verifiably true about this agent. + +Everything built afterwards is confined to that contract. The environment may only implement +tools listed in it. A scenario may only reference values grounded in it. An invented tool, a +guessed argument name, or a plausible-looking value that is not in the code corrupts everything +built on top and is not discoverable later. + +When in doubt, ask. You are talking to a person and they can answer. + +## Talking + +Answer what they ask, briefly and in plain language. Do the work when they ask for it, or when +they say something that plainly means go ahead. Do not start a long piece of work because +somebody greeted you. + +Keep replies short. They can see every tool you call and what it answered, so do not narrate +what is already on their screen. + +## How to read + +Start from the entry point and follow the registrations, not the documentation. README files and +docstrings describe intent; the contract records behaviour. Where they disagree, the code wins +and the disagreement is worth mentioning. + +Find, in roughly this order: + +1. **The tools.** Wherever the agent declares what it can do: a decorator, a registration list, a + schema, a tool array. Record the exact callable name the model would emit, not a friendly + label. + +2. **Argument names and types.** Read the signature. An argument declared as a list is a + different tool from one declared as a single value, and an environment built on the wrong one + fails at the first call. Record types wherever the source states them. + +3. **Argument values.** Where an argument is constrained to a set, an enum, a literal union, or a + lookup into fixed data, record the real values. + +4. **The rules.** Hard constraints the agent is instructed or coded to obey. Prefer the exact + wording from its system prompt or its validation code. These matter: the agent under test is + told them and graded against them, and its prompt is where most of them live. Prompts are + often kept away from the main agent file, so search the whole source for a long instructions + string before concluding there are none. + +5. **The modality.** How a person reaches this agent: a voice session, a text interface, or a + browser it drives. This decides how it is later run, so getting it wrong reroutes every test. + Many agents can run more than one way and the code alone will not say which is being tested — + **ask** rather than guessing. + +6. **What it depends on.** Everything the agent reaches for that has to exist before it can + work: a datastore, a service it calls over HTTP, a file it reads, a queue. Record each one, + what it provides, and which tools cannot work without it. The environment stage builds these, + so a dependency you do not record is a tool that will have nothing to answer it. + +7. **Whether its tools have code, and how to reach it.** This is the difference between testing + the agent and testing somebody's reimplementation of it, so it is worth real effort. + + For each tool, find the function that actually runs and record where it lives and how it is + called: a module-level function, a method on a class, something hanging off an object that has + to be built first, or an endpoint already reachable over HTTP. Say which, per tool. Where a + tool takes the agent's own state as an argument, name that argument. + + Some tools cannot be reached at all. A framework may define them as closures inside a class, + so there is nothing importable. **Record that plainly rather than leaving the entry blank**: + the environment stage needs to know the difference between a tool it may write and a tool it + could not reach, and only the second one is worth asking you about. + +8. **How its code says no.** Code written for production often reports failure by returning a + value rather than raising, so a returned string can be a refusal. Read one or two of its tools + and record the convention. Without it, every refusal is recorded as a success, which hides the + behaviour most worth testing. + +9. **What it takes to run.** Its install command from its own lockfile or requirements, the + language and version, where imports resolve from, and whether it has a Dockerfile of its own. + Its own Dockerfile is used in preference to anything written for it. + +10. **Its data store, and how the connection is chosen.** Which kind it is, and whether the + connection comes from an environment variable, a config file, or a constructor argument. Say + so if it is hardcoded: that is the difference between substituting a store cleanly and having + to change the agent's code, which is a decision for the person, not for you. + +7. **The data.** Where it lives, its shape, and its contents. Record the **shape** completely: + every field of every kind of record, and any values a field is constrained to. Record the + **contents** in proportion — a small dataset goes in whole; for a large one a representative + sample is what belongs here, chosen to include the awkward rows an agent has to cope with: a + record already cancelled, an item out of stock, an account with nothing on file. + + An exact replica is not the goal. Copying thousands of records through this stage loses + fidelity rather than gaining it. What is needed is enough for a world that exercises the same + flows and can refuse for the same reasons. + +8. **Use cases.** What this agent is *for*, one plain sentence each. "Cancel an order that has + not yet shipped." "Look up a customer by email." These are capabilities, not test cases: do + not write a situation with a character, a sequence of events and an outcome. Those are + scenarios and they are written later, from these sentences. + +## A repository may not hold one agent + +What you are pointed at is a directory, not necessarily a single agent. Before reading anything in +depth, work out what is actually in there. Three shapes come up: + +**One agent.** The ordinary case. Read it. + +**Several agents side by side.** A repository organised by domain or by product, each with its own +tools, its own rules and its own data. They may share a base class or a runner, which is what makes +this easy to miss: the shared parts look like the agent until you notice the tools differ per +directory. **List what you found and ask which one is being tested.** Do not pick. Building a +contract for the wrong one wastes every stage after it, and the person who pointed you here knows +which they meant. + +**One agent with several runtimes.** The same tools reachable over voice, over chat, or through a +browser. That is one agent, and what to ask about is the modality, not which agent. + +How to tell them apart: look for repeated structure. Several directories that each define their own +set of tools, their own instructions and their own data are several agents. Several entry points +over one set of tools are one agent with several runtimes. + +Say what you found either way, briefly, before you start reading in depth. "This holds four agents, +one per domain, which do you want" costs a turn and saves the whole stage. + +## When you are not sure + +You have `AskUserQuestion`. Use it whenever the source genuinely does not settle something and +the answer changes what gets built: which modality is under test, whether an argument is +required or optional, two mutually exclusive readings of a rule, data that looks like a +placeholder. + +Ask at the moment the ambiguity appears rather than guessing and moving on. Anything nobody +answers goes in `open_questions`, so the gap is visible rather than hidden. + +Do not ask about anything the code answers. Reading one more file is cheaper than a question. + +## Finishing + +Call `submit_contract` with the whole contract as one flat object. It is validated when you call +it; if anything is wrong you get the full list back and you fix it and call again. + +Before you submit, check your own work once: open the source again for every tool you listed and +confirm the name, the arguments and the types are exactly as written there. A contract that is +structurally valid and factually wrong passes every automatic check and fails everything after. + +Then say briefly what this agent is, what it can do, and anything you were unsure about. diff --git a/harness/src/agent_harness/skills/write-scenarios/SKILL.md b/harness/src/agent_harness/skills/write-scenarios/SKILL.md new file mode 100644 index 0000000..a3a0d99 --- /dev/null +++ b/harness/src/agent_harness/skills/write-scenarios/SKILL.md @@ -0,0 +1,361 @@ +--- +name: write-scenarios +description: Write the scenarios an agent is tested with, each proved before it is kept. +--- + +# Write the scenarios + +You are writing tests for an AI agent. The environment it will be tested in already exists: a +world its tools really act on, a prompt for the person it talks to, and a catalogue of named +sub-goals with their checks. Your job is to write the individual tests. + +You are talking to a person. Answer what they ask, briefly, and do the work when they ask for +it. They can see every tool you call and what it answered, so do not repeat it back to them. + +## What a scenario is + +One test. It changes the world a little, gives the person a task, and names what must be true +afterwards. + +``` +name short identifier; it becomes this scenario's folder +use_case which of the agent's use cases this belongs to +tests one line: what this scenario is trying to find out +instruction the task, written to the person the agent is serving +persona who that person is: identity, communication style, languages/accent and characteristics +setup_code Python: def setup(world) — what this scenario changes first +ready_code Python: def ready(world) — is the world ready for this scenario +solution what a correct agent would do: [{tool, arguments}] +sub_goals names from the shared catalogue that must hold +``` + +**Persona and world condition are different things.** `persona` is the clean, structured profile +of the person making this request. It uses the existing voice-scenario shape: `name`, `gender`, +`age_group`, `occupation`, `location`, `personality`, `communication_style`, `keywords`, +`languages`, `accent`, `multilingual`, and free-form `metadata`. Use the details that change the +conversational risk being tested. `setup_code` is the world condition: the item +is out of stock, the record already exists, or the order has already shipped. Keep both grounded +in the requested test; do not invent backstory that changes nothing. + +## Three parts that must never leak into each other + +Getting this wrong is what makes a test worthless, and it is the most common way to write a +scenario that looks fine and measures nothing. + +| | What it is | What it must never contain | +|---|---|---| +| **instruction** | what the person on the other side is living through | the answer, the checks, or facts they could not know | +| **setup** | the world's condition | anything the person is supposed to say | +| **checks** | the hidden pass or fail rules | anything the agent was told | + +## Writing the instruction + +**The instruction is a circumstance, not a script.** Write it in the second person, as what this +person is living through: who they are, what is happening to them, and what they want. It is +never a list of lines to say, and never the agent's turns. + +``` +BAD Ask for . Then change your mind and ask for instead. + Confirm the total at the end. + (a stage direction. The person recites it, and the run measures whether the + agent can follow dictation. Nothing about the change of mind is tested, + because it arrives exactly when the script says so) + +GOOD You want , and you are not particular about . Partway through, you realise is what you actually + need, and you would rather swap than end up with both. + (a situation. What they say is theirs to work out, and the agent has to cope + with a change of mind arriving mid-conversation rather than on cue) +``` + +Written with placeholders on purpose. Fill them from **this** agent's own data, and never from a +worked example of another agent. + +**What they know but will not volunteer goes in its own paragraph**, marked as such: *"You know +the reference for it, but you will only give it if asked."* The whole point of many scenarios is +whether the agent asks. Put that in the instruction and the agent gets it for free; +leave it out entirely and the scenario cannot be completed. + +**Knowing a value and volunteering it are separate choices.** The person must *possess* every +value the agent could legitimately ask for; whether they offer it unprompted is the scenario's +decision. Those are different sentences and only the second is optional. + +### What this person is known by + +Many agents establish who they are dealing with before they will act. Give that its own short +section at the end of the instruction, and **read every value out of the world with +`inspect_world` first**. Never invented, never carried over from another scenario: the record has +to be the one the agent's own lookup will actually find. + +Four rules, and each one has cost a whole run: + +**Cover every route, not the one you expect.** Where an agent can establish something more than +one way, which way it takes is not yours to choose. An instruction carrying the values for one +route is complete right up until that route fails, and then the conversation stops at the front +door with the person unable to answer a question they plainly should be able to answer. +Alternatives exist precisely because the first way sometimes does not work. + +**Say what each value is for.** Where a scenario involves two values of the same shape in +different roles, the current one and the replacement, the account's and the order's, give both +and name the role of each. Handed only one, the person will offer it for the other purpose, +because it is the only such value they have. That value is real, it appears in the instruction, +and it still fails, which makes it far harder to diagnose than a missing value: everything on +screen looks correct. + +**Take them all from one record.** Fields from two different records describe somebody who does +not exist, and no lookup will ever find them. + +**Possessing and volunteering are separate.** Whether the person offers a value unprompted is the +scenario's business. Whether they have it at all is not optional. + +**Use persona deliberately.** An accent, personality or characteristic belongs in `persona` only +when it changes the conversational risk being exercised. A rude customer is a different scenario +from a polite one only if the agent must handle that difference. Persona never contains the +answer, hidden checks or values the person has not been given. Every conversational scenario must +supply one when the simulator prompt asks for `{{ persona }}`. Before submitting, fill its +required profile: `name`, `personality`, `communication_style`, `languages`, `accent`, and at +least one `keywords` entry. The harness rejects an incomplete persona rather than quietly generating a +generic caller. + +## Writing setup, and the mistake to avoid + +**Whatever the instruction presumes about the world, setup has to make true.** This is where +scenarios most often go wrong: the instruction says the person is returning an order that has +already shipped, and setup leaves every order pending, so the agent refuses correctly and the +scenario fails it for being right. + +The rule: read your own instruction back, list every condition it assumes, and make sure `setup_code` +establishes each one and `ready_code` proves it. An empty `setup_code` is only honest when the base world +already holds everything the instruction presumes. + +## Two scenarios are different only if the right answer differs + +Not if the wording differs. "The item is in stock" and "the item is out of stock" are two +scenarios, because the correct outcome is different. Two polite requests for the same thing are +one scenario written twice. + +## The bar every scenario has to clear + +- **A competent agent could plausibly fail it.** If any correct implementation passes for free, it + teaches nothing. Do not write it. +- **A real person could plausibly bring this situation.** Nothing contrived. +- **Every concrete value is real**, taken from the contract or the world. An invented id or menu + item makes the test worthless whatever else it does. + +## Plan the whole suite before writing any of it + +Writing scenarios one at a time produces a suite that clumps: five variations on the easy path and +nothing on the parts that break. So partition the work first, out loud, before the first +`submit_scenario`. + +Say how many scenarios each use case gets, **in proportion to how much can genuinely go wrong in +it**. A use case with rules to enforce, information to gather, or state to change earns a large +share; one where little can fail earns one scenario or none. Then, for each use case, name the +distinct **angles** you will write: the ordinary path, the branch that cannot be completed, the +rule under pressure, the state that has to carry, the same request against a differently seeded +world. + +Show that plan to the person and let them redirect it. It costs one turn and it is the difference +between twenty tests and twenty rewordings of four. + +## Write from more than one point of view + +A suite written from a single vantage point tests a single vantage point, however many scenarios +it has. Left alone, anyone writing tests drifts toward the ones they thought of first, which are +usually the ones the agent was built for. + +So work the plan from several stances in turn, and say which one each scenario came from. These +are the ones that reliably find different things: + +- **The engineer who built it**, testing what they know is fragile in their own code: the branch + with the most conditions, the operation that cannot be repeated, the value that is validated in + one place and not another. +- **The adversary**, hunting requests that sit exactly on a rule's edge: the thing just barely not + permitted, the request that is fine on its own and forbidden in this state, the pressure to skip + a step the rules require. +- **The newcomer**, who does not know the agent's vocabulary and asks in their own words: names + the thing wrongly, gives a value in a form nobody expected, does not know which of two things + they have. +- **The operator**, recreating what production traffic actually produces: a record already in an + awkward state, a request about something that has already been dealt with, the same thing asked + twice. +- **The product owner**, testing the promises made about this agent one at a time: for each thing + it claims to do, a scenario where doing it correctly is the whole question. + +Every stance still obeys the bar above: a real person could bring it, a competent agent could +fail it, and the values are real. A stance chooses *what to look at*, never whether the scenario +has to be honest. + +Two rules keep this from turning into noise. **Each scenario carries one use case, and no two +scenarios carry the same one** — a duplicate is either the same test twice or one of them is +mislabelled, and it hides a gap while appearing to fill it. And a stance that produces nothing new +for a given agent produces nothing: an agent with no rules to bend does not need an adversarial +scenario invented for it. + +## Organise by use case, then by branch + +A login flow is not one row with the happy path and the edge cases inside it. It is several: +login with a password, login with a provider, forgotten password, account locked. Do the same +here. Find the agent's real use cases and let their branches be the scenarios. + +**Different outcomes are different scenarios.** The customer who accepts a substitute and the +customer who refuses one are two rows, not one. + +## The three gates + +Every scenario is put through these before it is kept. You are told which one failed. + +**1. Ready.** The world is restored, your `setup_code` runs, then your `ready_code`. The world +must end up holding what your scenario presumes. + +This is the one people skip and it is the one that saves you. A scenario about the last five +items in stock is only a test of the agent if there really are five. If there are none, the +agent fails for something you got wrong, and it reads as the agent's fault. `ready_code` is how +you make that impossible. + +**2. Solvable.** Your reference solution is played through that world and the checks of every +sub-goal you named must pass. If they do not, either the scenario cannot be passed at all or a +check is wrong. + +**3. Not vacuous.** The same checks run again with nothing done, and must fail. A check that +passes while the agent does nothing grades nothing while reporting a result. + +Gate 3 has a common trap. If your scenario is about something that must *not* happen, checking +the world alone cannot show it: an untouched world looks exactly like one where the agent +correctly refused. Check the calls instead — that the agent tried, and that the attempt was +refused rather than succeeding. + +## Writing setup_code + +Python defining `setup(world)`. Leave it empty when the base world is already right. + +**Write every setup against the base world, never against a scenario you wrote before it.** At run +time each scenario restores its own copy of the frozen base and applies only its own setup, so +nothing another scenario did is there. This is easy to get wrong while writing several in a row: +you have just set an order to "delivered" for one scenario, and the next one reads as though that +still holds. It does not. If a scenario needs a record in a particular state, its own setup puts +it there, whatever any earlier scenario happened to do. The same goes for the calls you make while +rehearsing with `try_calls`: those run on a throwaway copy and change nothing anybody else sees. + +You have two ways to change things, and **neither of them names what the world is kept in**. A +scenario that wrote SQL would only work against a world that happened to be a database, and the +store is the thing that varies most between agents. + +**Prefer the agent's own tools.** It goes through the same path the agent will, so anything the +world would refuse to you would have refused the agent too. + +```python +def setup(world): + world.call("add_to_stock", {"item_id": "widget", "quantity": 5}) +``` + +**Otherwise change the world directly**, in collections and records: + +```python +world.put(collection, record, key=...) # add one record +world.change(collection, key, changes, by=...) # change one record +world.drop(collection, key, by=...) # remove one, or all of them with no key +``` + +The keyed-on argument names the column a table is keyed on, and is not needed for a collection +that is keyed already. `world.state()` shows you every collection and what is in it, which is how you find out +which you are dealing with. + +```python +def setup(world): + world.change("stock", "widget", {"quantity": 5}, by="item_id") +``` + +Use the direct route only for states no tool can produce: a record already in a condition the +agent could never create itself. + +## A collection is not always a list + +`world.state()` gives every collection this world has, and their shapes differ by agent. A table +gives a list of records. A collection the agent's own code keeps is often a mapping keyed by +identifier, and iterating that yields the keys, which are strings, so reading a field off one fails. + +```python +held = world.state()["some_collection"] +records = list(held.values()) if isinstance(held, dict) else held +``` + +Look before you write. `inspect_world` shows you which is which, and this applies to `setup_code`, +`ready_code` and every check. + +## Writing ready_code + +Python defining `ready(world)`. Return `None` when the world holds what the scenario presumes, +or a sentence naming what is missing. + +Check the thing your scenario actually depends on, not everything. + +```python +def ready(world): + rows = world.state()["stock"] + widget = next((r for r in rows if r["item_id"] == "widget"), None) + if widget is None: + return "no widget in stock at all; this scenario is about its last five" + if widget["quantity"] != 5: + return f"stock says {widget['quantity']} widgets, this scenario needs exactly 5" + return None +``` + +## The solution is not optional + +Every scenario carries what a correct agent would do. It is never run against the agent under +test. It exists to prove the scenario can be passed at all, and it is what gate 2 uses. + +Work it out with `try_calls` before you submit. Run the calls, pass your `setup_code` so you see +the world the agent would actually face, look at the state they leave, and confirm the sub-goals +you are naming respond to it. + +## Reuse the sub-goals + +Name entries from the shared catalogue. Do not restate them in your own words, and do not invent +a new one where an existing one means the same thing. That sharing is what lets results add up: +the same sub-goal failing in seven of twelve scenarios is one sentence somebody can act on. + +If something genuinely needs checking and no entry covers it, add one with `add_sub_goal`, with +its check in code. Prefer code over a judged check — you have the world afterwards and every +call with its arguments, and most things worth checking are visible in one of them. + +## What makes a suite worth running + +Spread across these. Ten happy paths tell you nothing you did not already know. + +- **The ordinary branch**, done cleanly. You need a baseline. +- **The branch that cannot be completed**: the item is not there, the record does not exist, the + option is outside what the tool accepts. The right behaviour is to refuse clearly and offer + what is possible. +- **The rule under pressure**: the person pushes for something a hard rule forbids, twice. + Giving way under pressure is the failure most worth catching. +- **State that has to carry**: do something, change your mind, undo it, confirm. The agent has to + know what it did two turns ago. +- **The same use case with the world seeded differently.** In stock and out of stock are two + rows, not one. + +## If the contract is wrong + +You will sometimes find that the agent's contract does not match what the world does — a tool +that accepts a value it was not recorded as accepting, a rule that is not really a rule. Correct +it with `amend_contract`, `add_rule`, `drop_rule` or `fix_tool` and say why. Every amendment is +recorded on the contract. + +Never work around a contract you believe is wrong. A scenario written to dodge a bad contract +hides the problem and everything built afterwards inherits it. + +## How to work + +1. `inspect_world` with no table, then look at the ones that matter. Read the sub-goals already + defined. +2. Read the agent's hard rules. Each one is a branch waiting to be written. +3. For each scenario: work out the solution, `try_calls` it with your `setup_code`, then + `submit_scenario`. +4. Read what comes back. A refusal names which gate failed and why. +5. `save_scenarios` when you have the number that was asked for. + +## Finishing + +Say what the suite covers and what it does not, which sub-goals carry the most scenarios, and +name anything you could not test because the environment or the contract does not support it. diff --git a/harness/src/agent_harness/sources.py b/harness/src/agent_harness/sources.py new file mode 100644 index 0000000..a3ca097 --- /dev/null +++ b/harness/src/agent_harness/sources.py @@ -0,0 +1,187 @@ +"""Where an agent comes from, and how a session reaches it. + +A folder of source code is one kind of agent, not the only kind. The same agent may arrive as a +provider connection with a system prompt and a tool schema, as a platform definition, or as a +spec somebody pasted in. The stage that reads an agent is the same in all of those cases; what +differs is where it looks and what it is allowed to touch. + +So the method stays in the skill and the location lives here. Supporting a new kind of agent is +registering one class, not editing any stage. +""" + +from __future__ import annotations + +import json +import re +import subprocess +from collections.abc import Callable +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Protocol + + +class AgentSource(Protocol): + """Everything a stage needs in order to reach one agent.""" + + kind: str + name: str + + def workdir(self) -> Path: + """The directory the session runs in.""" + + def builtin_tools(self) -> tuple[str, ...]: + """Built-in tools this source needs granted.""" + + def servers(self) -> dict[str, Any]: + """In-process tool servers this source provides, if any.""" + + def briefing(self) -> str: + """What to tell the model about where this agent's truth lives.""" + + +@dataclass +class RepoSource: + """An agent that exists as source code on disk.""" + + name: str + root: Path + kind: str = "repo" + + def workdir(self) -> Path: + return self.root + + def builtin_tools(self) -> tuple[str, ...]: + return ("Read", "Glob", "Grep") + + def servers(self) -> dict[str, Any]: + return {} + + def briefing(self) -> str: + return ( + f"This agent is a repository at {self.root}. Its truth is the source code: the tool " + "registrations, the function signatures, the validation logic, and whatever holds " + "its data. Read it with Read, Glob and Grep. Documentation describes intent; the " + "code describes behaviour, and where they disagree the code wins." + ) + + +@dataclass +class GitHubSource(RepoSource): + """A public GitHub repository cloned into this harness session.""" + + url: str = "" + kind: str = "github" + + def briefing(self) -> str: + return ( + f"This agent was cloned from {self.url or 'GitHub'} into {self.root}. Its truth is " + "the cloned source code: the tool registrations, function signatures, validation " + "logic, and whatever holds its data. Read it with Read, Glob and Grep." + ) + + +_GITHUB_REPOSITORY = re.compile( + r"^https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:\.git)?$" +) + + +def clone_github_repository(url: str, destination: Path) -> Path: + """Shallow-clone one public GitHub repository into a session-owned directory.""" + url = url.strip().rstrip("/") + if not _GITHUB_REPOSITORY.fullmatch(url): + raise ValueError("use a public HTTPS GitHub repository URL such as https://github.com/owner/repo") + if destination.exists(): + raise ValueError(f"the session source directory already exists: {destination}") + + destination.parent.mkdir(parents=True, exist_ok=True) + completed = subprocess.run( + ["git", "clone", "--depth", "1", url, str(destination)], + capture_output=True, + check=False, + text=True, + ) + if completed.returncode: + detail = completed.stderr.strip() or "git clone failed" + raise RuntimeError(detail) + return destination + + +@dataclass +class SpecSource: + """An agent supplied directly as a prompt and a tool schema, with no repository. + + This is the shape a hosted provider gives back, so it is also the fallback whenever a + connection can be read once and handed over as text. + """ + + name: str + system_prompt: str + tool_schema: list[dict[str, Any]] = field(default_factory=list) + data: dict[str, Any] = field(default_factory=dict) + scratch: Path = Path(".") + kind: str = "spec" + + def workdir(self) -> Path: + return self.scratch + + def builtin_tools(self) -> tuple[str, ...]: + return () + + def servers(self) -> dict[str, Any]: + return {} + + def briefing(self) -> str: + parts = [ + "This agent is supplied as a definition, not a repository. Everything knowable " + "about it is below; there is no code to open, so do not guess at anything absent.", + f"SYSTEM PROMPT:\n{self.system_prompt}", + ] + if self.tool_schema: + parts.append( + f"TOOL SCHEMA:\n{json.dumps(self.tool_schema, indent=2)[:6000]}" + ) + if self.data: + parts.append(f"DATA:\n{json.dumps(self.data, indent=2)[:6000]}") + return "\n\n".join(parts) + + +_REGISTRY: dict[str, Callable[..., AgentSource]] = { + "repo": lambda **kw: RepoSource(name=kw["name"], root=Path(kw["root"])), + "github": lambda **kw: GitHubSource( + name=kw["name"], root=Path(kw["root"]), url=kw.get("url", "") + ), + "spec": lambda **kw: SpecSource( + name=kw["name"], + system_prompt=kw.get("system_prompt", ""), + tool_schema=kw.get("tool_schema") or [], + data=kw.get("data") or {}, + scratch=Path(kw.get("scratch", ".")), + ), +} + + +def register_source(kind: str, factory: Callable[..., AgentSource]) -> None: + """Add a kind of agent. A provider connection is a class and one line here.""" + _REGISTRY[kind] = factory + + +def resolve(kind: str, **kwargs: Any) -> AgentSource: + if kind not in _REGISTRY: + raise NotImplementedError( + f"no agent source of kind {kind!r}; registered kinds are " + f"{', '.join(sorted(_REGISTRY))}" + ) + # An empty root used to resolve to the current directory, which is worse than failing: every + # later stage then reads a real path, finds the harness's own repository, and reports that the + # agent has no code on disk. Nothing downstream can tell that apart from an agent that really + # was given as a specification. + if "root" in kwargs and not str(kwargs.get("root") or "").strip(): + raise ValueError( + f"a {kind!r} source needs the path its code lives at, and none was given. If this " + "agent has no code on disk, it is not this kind of source." + ) + return _REGISTRY[kind](**kwargs) + + +def supported() -> tuple[str, ...]: + return tuple(sorted(_REGISTRY)) diff --git a/harness/src/agent_harness/tools.py b/harness/src/agent_harness/tools.py new file mode 100644 index 0000000..3256204 --- /dev/null +++ b/harness/src/agent_harness/tools.py @@ -0,0 +1,585 @@ +"""The tools the harness offers a session, and the gates behind them. + +The model does judgement; these do the parts that must be exact. Validation lives inside the +tool rather than after the session, so a problem is returned into the conversation and fixed on +the next turn instead of surfacing once the session is already over. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from claude_agent_sdk import create_sdk_mcp_server, tool + +from .contract import MODALITIES, AgentContract, validate_contract + +CONTRACT_SERVER = "contract" + + +def _ok(text: str) -> dict[str, Any]: + return {"content": [{"type": "text", "text": text}]} + + +# validate_contract returns short codes: they are stable, testable, and the same string every +# time. What a code means is a separate question, and answering it here keeps the codes exact +# while the message the model reads says what to actually do. +_GUIDANCE = { + "empty:agent": "the `agent` field is empty. A short lower-case name; it is only the " + "artifact folder's label", + "no-tools": "the `tools` field is empty. List the agent's real tools; nothing downstream " + "can be built without them", + "no-use-cases": "the `real_use_cases` field is empty — note the name, it is not " + "`use_cases`. List the concrete situations this agent handles, from its tools and data", + "no-arguments-on-any-tool": "every tool was recorded with no arguments, which means they " + "were read and not written down. Put each tool's exact parameter names in args", + "duplicate-tool-names": "the same tool is listed twice; keep one entry per tool", + "types-for-unknown-args": "arg_types names an argument that is not in args. The names must " + "match the source exactly", +} + + +def _advice(code: str) -> str: + for key, said in _GUIDANCE.items(): + if code.startswith(key) or key in code: + return f"{code} — {said}" + return code + + +def _problems(problems: list[str], arrived: list[str] | None = None) -> dict[str, Any]: + """Every problem at once, each with what to do about it. + + All of them together, never one at a time: a gate that reveals the next problem only after + the last is fixed costs a full turn per problem and reads as though the rules are being + invented as it goes. + + When the fields arrived under names this does not recognise, it says which names it got. + Without that the answer is "agent is empty, there are no tools" about a submission that + contained both, and the only way out is guessing at the packaging. + """ + said = "Not accepted. Fix all of these and call submit_contract again:\n - " + ( + "\n - ".join(_advice(problem) for problem in problems) + ) + unrecognised = arrived is not None and not any( + key in arrived for key in ("agent", "tools", "real_use_cases") + ) + if unrecognised: + said += ( + f"\n\nWhat arrived was: {', '.join(arrived) or '(nothing)'}. None of those are " + "contract fields, so the fields were probably nested inside something or sent as " + "one JSON string. Send them as the tool's own top-level arguments — agent, tools, " + "real_use_cases and the rest — not wrapped in an outer object." + ) + return { + "content": [{"type": "text", "text": said}], + "is_error": True, + } + + +_CONTRACT_KEYS = ("agent", "tools", "real_use_cases", "one_liner", "hard_constraints") + + +def _looks_like_a_contract(value: Any) -> bool: + return isinstance(value, dict) and any(key in value for key in _CONTRACT_KEYS) + + +def unwrapped(payload: dict[str, Any]) -> dict[str, Any]: + """The contract itself, however it was packaged. + + A contract is a nested thing being described, so it arrives wrapped — ``{"contract": {...}}`` + — or stringified, as JSON in a single argument, often enough to matter. In both the fields + are present and correct and only the packaging is wrong. Rejecting that teaches nothing + about the agent and costs a full turn, so it is unpacked; only an object that actually looks + like a contract is unwrapped, so a real field that happens to hold a dict is never mistaken + for an envelope. + """ + if not isinstance(payload, dict): + payload = {} + if any(key in payload for key in ("agent", "tools", "real_use_cases")): + return payload + for value in payload.values(): + if _looks_like_a_contract(value): + return value + if isinstance(value, str): + text = value.strip() + if text.startswith("```"): + # Fenced JSON: the model wrote it as it would in a message. + text = text.strip("`").removeprefix("json").strip() + if not text.startswith("{"): + continue + try: + parsed = json.loads(text) + except json.JSONDecodeError: + continue + if _looks_like_a_contract(parsed): + return parsed + for inner in parsed.values() if isinstance(parsed, dict) else []: + if _looks_like_a_contract(inner): + return inner + return payload + + +def accept_contract(payload: dict[str, Any], destination: Path) -> dict[str, Any]: + """The gate itself: validate, and write only if it passes. + + A plain function rather than only a tool body, so the rule that decides whether a contract + is usable can be exercised and reasoned about without standing up a session. + """ + arrived = sorted(payload) if isinstance(payload, dict) else [type(payload).__name__] + payload = unwrapped(payload) + try: + contract = AgentContract.model_validate(payload) + except Exception as invalid: + return _problems([f"schema:{invalid}"[:600]], arrived) + + problems = validate_contract(contract) + if problems: + return _problems(problems, arrived) + + destination.mkdir(parents=True, exist_ok=True) + path = destination / "contract.json" + path.write_text( + json.dumps(contract.model_dump(), indent=2, ensure_ascii=False), + encoding="utf-8", + ) + return _ok( + f"Accepted and saved to {path}.\n" + f"{len(contract.tools)} tools: {', '.join(sorted(contract.tool_names()))}\n" + f"{len(contract.hard_constraints)} rules, " + f"{len(contract.real_use_cases)} use cases, " + f"{len(contract.open_questions)} open questions." + ) + + +def contract_tools(destination: Path) -> Any: + """A server exposing ``submit_contract``, writing to ``destination`` on acceptance.""" + # Each of these is a nudge, not a wall: the first submission missing something that is + # nearly always there gets sent back with directions, and a second submission is accepted. + # A gate with no way through would permanently block the rare agent that genuinely lacks it, + # and this stage cannot tell those two apart from the outside. + nudged: set[str] = set() + + @tool( + "submit_contract", + "Submit the agent's testing contract: everything verifiably true about this agent, as " + "one flat object. Every field is described in the schema; fill in what the source " + "supports and leave the rest out.\n\n" + "It is validated when you call it. If anything is wrong you get the whole list back at " + "once, in terms of what to fix, and you submit again.", + # Nothing required, and that is deliberate. This layer runs before the tool body, so + # anything it rejects never reaches the code that could have understood it — a contract + # sent inside a wrapper is complete and correct, and is unwrapped a few lines below, but + # only if it gets there. accept_contract is the single gate; it reports every problem at + # once and says what to do about each. + # + # The descriptions are the point of this block. The schema is shown to the model before + # it calls anything, so what is written here is the difference between a correct first + # call and a sequence of rejected guesses. + schema( + { + "agent": { + "type": "string", + "description": "Short lower-case identifier, no spaces. Only a label for " + "the artifact folder.", + }, + "one_liner": { + "type": "string", + "description": "One sentence: what this agent is for.", + }, + "modality": { + "type": "string", + "enum": list(MODALITIES), + "description": "How a person reaches it, read from its runtime. A voice " + "session (LiveKit, telephony, TTS/STT) is voice; a text interface is chat; " + "a browser-driving agent is browser. This decides how it is later run.", + }, + "conversational": { + "type": "boolean", + "description": "True if a person talks with it turn by turn. False for an " + "agent given one task and left to it.", + }, + "system_prompt_excerpt": { + "type": "string", + "description": "The agent's own instructions, quoted. Often lives away from " + "the main agent file.", + }, + "hard_constraints": { + "type": "array", + "items": {"type": "string"}, + "description": "Rules it must obey, in the source's own words. The agent " + "under test is told these and graded against them.", + }, + "tools": { + "type": "array", + "description": "Every tool the agent really has. Everything downstream is " + "built from these, so a tool without its arguments cannot be tested.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The exact callable name the model emits.", + }, + "args": { + "type": "array", + "items": {"type": "string"}, + "description": "Exact parameter names, in order.", + }, + "arg_types": { + "type": "object", + "description": "Declared type per argument where the source " + 'states one: {"recipient_ids": "list[str]"}.', + }, + "arg_values": { + "type": "object", + "description": "Real permitted values per argument where it is " + "constrained to a set, an enum or a lookup: " + '{"priority": ["low", "normal", "urgent"]}.', + }, + "description": {"type": "string"}, + }, + # Nothing required: a tool genuinely taking no arguments is ordinary, + # and requiring args here rejects the whole contract because of one. + # That every tool has none is the real defect, and validate_contract + # is where it is caught, with an explanation. + }, + }, + "data_schema": { + "type": "object", + "description": "The shape of the records the agent works on: which fields " + "each kind of record has.", + }, + "base_environment": { + "type": "object", + "description": "Its real starting data, reproduced exactly — including " + "anything that looks like a mistake. The world is a replica, not a " + "corrected version.", + }, + "dependencies": { + "type": "array", + "description": "Everything this agent reaches for that has to exist before " + "it can work, so the next stage knows what to build. A datastore, a service " + "it calls over HTTP, a file it reads, a queue it publishes to. The world is " + "a sandbox and nothing reaches outside it, so each of these is built inside " + "it — the agent's call goes to something real that happens to be ours.", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "kind": { + "type": "string", + "description": "datastore, service, file, queue, or whatever " + "this actually is.", + }, + "what": { + "type": "string", + "description": "What it holds or answers, and what the agent " + "needs from it.", + }, + "used_by": { + "type": "array", + "items": {"type": "string"}, + "description": "The tools that cannot work without it.", + }, + }, + }, + }, + "real_use_cases": { + "type": "array", + "items": {"type": "string"}, + "description": "What this agent is for, one plain sentence each. These are " + "capabilities, not test cases: 'cancel an order that has not shipped', not " + "a narrated situation with a customer, a name and an outcome. Scenarios are " + "written later, from these.", + }, + "notes": { + "type": "string", + "description": "Free-form, yours. Anything else worth carrying forward: " + "quirks, traps, a plausible name that does not exist, an id that looks like " + "a typo but is real. Shown verbatim to every later stage.", + }, + "open_questions": { + "type": "array", + "items": {"type": "string"}, + "description": "What the source did not settle and you could not ask about.", + }, + "implementation": { + "type": "string", + "enum": ["present", "absent", "partial"], + "description": "Whether the agent ships working code for its tools, as " + "opposed to only declaring them. The environment runs the agent's own code " + "wherever it exists, so this decides whether anything gets written for it.", + }, + "tool_entrypoints": { + "type": "array", + "description": "How to reach the agent's own implementation of each tool. " + "One entry per tool that has code. Without this the environment has to write " + "a replacement, which tests our reading of the agent instead of the agent.", + "items": { + "type": "object", + "properties": { + "tool": { + "type": "string", + "description": "The tool name, exactly as in `tools`.", + }, + "mode": { + "type": "string", + "enum": ["import", "construct", "service", "generate"], + "description": "import: a module-level function or a method on a " + "class, reachable directly. construct: it hangs off an object " + "that has to be built first. service: it is already reachable " + "over HTTP. generate: there is no implementation, so one has to " + "be written. Choose generate only when nothing can be reached.", + }, + "module": { + "type": "string", + "description": "Importable path as the agent's own code would " + "write it, e.g. package.module.file. Not a filesystem path.", + }, + "callable": { + "type": "string", + "description": "What to call inside that module. May be dotted " + "to reach a method on a class, e.g. TheClass.the_method.", + }, + "factory": { + "type": "string", + "description": "For construct: the expression that builds the " + "object, including whatever it needs to be constructed with.", + }, + "first_arg": { + "type": "string", + "description": "If the callable takes the agent's own state as " + "its first argument, its name. Empty when the callable opens its " + "own connection instead.", + }, + "notes": { + "type": "string", + "description": "Anything about reaching it that the fields above " + "do not carry, especially why a tool cannot be reached.", + }, + }, + }, + }, + "refusal_signature": { + "type": "string", + "description": "How this agent's own code says no in a value it returns " + "rather than by raising, described so it can be recognised, e.g. a string " + "beginning with a particular marker. Production code often reports failure " + "this way, and without this a refusal is recorded as a success, which hides " + "the behaviour most worth testing.", + }, + "data_store": { + "type": "object", + "description": "What the agent's tools read and write, and how to point them " + "at a different one.", + "properties": { + "kind": { + "type": "string", + "description": "postgres, clickhouse, mysql, sqlite, in_process for " + "state held in memory, or none.", + }, + "configured_by": { + "type": "string", + "description": "How the code chooses its connection: the environment " + "variable it reads, the config file, or the constructor argument. " + "This is what makes substituting a store possible without editing " + "the agent, so say if it is hardcoded.", + }, + "schema_from": { + "type": "string", + "description": "Where the schema comes from: its migrations, a DDL " + "file, its ORM models.", + }, + "loaded_by": { + "type": "string", + "description": "The agent's own loader, if it has one that builds " + "its starting data, as module and callable.", + }, + "loader_module": { + "type": "string", + "description": "The module that loader is imported from, so it can " + "be called rather than reimplemented.", + }, + "version": { + "type": "string", + "description": "The engine version, where the agent pins one.", + }, + "config_key": { + "type": "string", + "description": "Where a config file holds the connection instead, as " + "a dotted path such as database.url.", + }, + "host": { + "type": "string", + "description": "The host the agent expects. Record it even when it " + "is hardcoded: a hardcoded name is not a dead end, it is a name our " + "store can answer to.", + }, + "port": { + "type": "integer", + "description": "The port it expects.", + }, + "database": { + "type": "string", + "description": "The database name it expects. Ours is created with " + "exactly this name rather than the agent being changed.", + }, + "user": { + "type": "string", + "description": "The user it connects as.", + }, + "password_from": { + "type": "string", + "description": "Where the password comes from, never the password " + "itself. A contract is written to disk and read by people, so a " + "secret in it outlives the run that needed it.", + }, + }, + }, + "runtime": { + "type": "object", + "description": "What it takes to run the agent's code.", + "properties": { + "language": {"type": "string"}, + "version": {"type": "string"}, + "install": { + "type": "string", + "description": "Its own install command, e.g. from its lockfile or " + "requirements. Used as written rather than guessed at.", + }, + "workdir": { + "type": "string", + "description": "Where in the source imports resolve from, if not the " + "root.", + }, + "dockerfile": { + "type": "string", + "description": "Path to its own Dockerfile, if it has one. Theirs is " + "used in preference to anything written for it.", + }, + }, + }, + }, + [], + ), + ) + async def submit_contract(args: dict[str, Any]) -> dict[str, Any]: + payload = unwrapped(args) + + thin = [ + ( + "prompt", + bool(payload.get("conversational", True)) + and not payload.get("hard_constraints") + and not str(payload.get("system_prompt_excerpt") or "").strip(), + "no hard_constraints and no system_prompt_excerpt, for a conversational agent. " + "Its prompt usually exists and often lives away from the main agent file — " + "search the whole source for a long instructions string before deciding there " + "is none.", + ), + ( + "data", + bool(payload.get("tools")) + and not payload.get("data_schema") + and not payload.get("base_environment"), + "no data_schema and no base_environment, for an agent that has tools. The world " + "every test runs against is built from exactly these two, so without them the " + "next stage has no schema to create and no rows to seed, and every tool call it " + "makes will refuse. Record the shape of each kind of record the tools read or " + "write, and enough real rows to reach every branch those tools have — a " + "representative sample for a large dataset, the whole thing for a small one.", + ), + ] + # All of them together, and each only once. Nudging in sequence would cost a turn per + # nudge and read as though the requirements were being invented one at a time. + say = [said for key, when, said in thin if when and key not in nudged] + nudged.update(key for key, when, _ in thin if when) + if say: + return _problems( + say + ["If any of these genuinely does not apply, submit again as is."] + ) + return accept_contract(payload, destination) + + return create_sdk_mcp_server( + name=CONTRACT_SERVER, version="0.1.0", tools=[submit_contract] + ) + + +_JSON_TYPES = { + str: "string", + int: "integer", + float: "number", + bool: "boolean", + list: "array", + dict: "object", +} + + +def schema(properties: dict[str, Any], required: list[str]) -> dict[str, Any]: + """A tool's inputs, described well enough to be filled in correctly the first time. + + Two things this exists for. + + **Required means required.** Handing the decorator a plain ``{name: type}`` mapping marks + every parameter mandatory, so a tool with an optional field refuses any call that leaves it + out — "Input validation error: 'seed' is a required property" — for a field the tool itself + treats as optional. + + **A schema is documentation, not just validation.** It is shown to the model before it calls + anything, so a property carrying only ``{"type": "array"}`` says nothing about what belongs + in it, and the model discovers the shape by being rejected. That is a full turn per guess and + it is avoidable: pass a full JSON-schema fragment instead of a bare type wherever the shape + is not obvious from the name, and it is right on the first call. + + schema({"name": str, + "size": {"type": "string", "enum": ["S", "M", "L"]}}, ["name"]) + """ + wanted = list(required) + return { + "type": "object", + "properties": { + name: dict(kind) + if isinstance(kind, dict) + else _typed(_JSON_TYPES.get(kind, "string"), optional=name not in wanted) + for name, kind in properties.items() + }, + "required": wanted, + } + + +def _typed(kind: str, *, optional: bool) -> dict[str, Any]: + """One property's type, letting an optional field be null. + + Filling a field that does not apply with null is what a model does, and it is not wrong: the + alternative is inventing a value. Rejecting it costs a whole turn, and the rejection does not + even say which field was at fault: "None is not of type 'string'" is the entire message. + """ + return {"type": [kind, "null"]} if optional else {"type": kind} + + +def qualified(server: str, tool_name: str) -> str: + """The name an in-process MCP tool is granted under.""" + return f"mcp__{server}__{tool_name}" + + +def brief(value: Any, limit: int = 1800) -> str: + """What a call returned, shortened only when it has to be. + + Generous, and explicit when it cuts. A record from a real agent's data is long, and a reply + trimmed silently in the middle of it reads as though the field being looked for is absent: + the answer is then six more calls working around something that was there all along. + + Shared, because every stage that shows a caller what a tool answered has the same problem and + they were not agreeing about it: one showed 1800 characters and said when it cut, the other + showed 200 and said nothing, so the stage that most needs to read a record was the one that + could not. + """ + rendered = value if isinstance(value, str) else json.dumps(value, default=str) + if len(rendered) <= limit: + return rendered + return ( + rendered[:limit] + + f"\n... cut here, {len(rendered) - limit} more characters. Ask for one record rather " + "than many if you need the whole of it." + ) diff --git a/harness/src/agent_harness/understand.py b/harness/src/agent_harness/understand.py new file mode 100644 index 0000000..8d2fddd --- /dev/null +++ b/harness/src/agent_harness/understand.py @@ -0,0 +1,91 @@ +"""Stage one: read an agent and produce its contract. + +The stage is the same whatever the agent is. What changes between a repository, a provider +connection and a pasted definition is where the truth lives, and that comes from the source. + +It stays open after the first answer, because a contract is usually right on the second look and +not the first. Correcting it is the next thing said, not a re-run. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Callable + +from .config import artifact_dir, load_skill, read_only_session +from .contract import AgentContract +from .session import Stage +from .sources import AgentSource +from .tools import CONTRACT_SERVER, contract_tools, qualified + +SKILL = "understand-agent" + + +def open_stage( + source: AgentSource, + *, + out: Path | None = None, + ask: Callable[..., Any] | None = None, + max_turns: int = 70, +) -> tuple[Stage, Path]: + """A live understand-the-agent stage, and where it will write.""" + destination = out or artifact_dir(source.name) + options = read_only_session( + system_prompt=f"{load_skill(SKILL)}\n\n## This agent\n\n{source.briefing()}", + cwd=source.workdir(), + mcp_servers={**source.servers(), CONTRACT_SERVER: contract_tools(destination)}, + extra_tools=[ + *source.builtin_tools(), + qualified(CONTRACT_SERVER, "submit_contract"), + ], + max_turns=max_turns, + ) + if ask is not None: + options.can_use_tool = ask + return Stage(options, name=SKILL), destination + + +def opening(source: AgentSource) -> str: + # The name is only a label for the artifact folder, and saying so matters: told to "read + # the agent named verify_fix", a model went hunting the whole workspace for something + # called verify_fix instead of reading the path it was given. + return ( + "Read this agent and produce its contract. Where it lives is in your briefing; " + f"{source.name!r} is only the label its artifacts are filed under, not something to " + "search for.\n\n" + "Work through the tools, their exact argument names and types, the constrained argument " + "values, the rules it enforces, and its data. Ask me if the source genuinely does not " + "settle something that changes what gets built. Call submit_contract when you are done." + ) + + +def load(destination: Path) -> AgentContract | None: + """The contract on disk, if the stage produced one.""" + path = Path(destination) / "contract.json" + if not path.exists(): + return None + return AgentContract.model_validate(json.loads(path.read_text(encoding="utf-8"))) + + +async def understand( + source: AgentSource, + *, + out: Path | None = None, + follow_ups: list[str] | None = None, + on_event: Callable[..., Any] | None = None, + ask: Callable[..., Any] | None = None, + max_turns: int = 70, +) -> AgentContract | None: + """Run the stage start to finish and return the contract. + + ``follow_ups`` are corrections applied in the same session, the scripted equivalent of an + operator typing them. ``ask`` handles clarifying questions; without it the model records what + it could not resolve in ``open_questions`` instead of blocking. + """ + stage, destination = open_stage(source, out=out, ask=ask, max_turns=max_turns) + async with stage: + await stage.say(opening(source), on_event=on_event) + for follow_up in follow_ups or []: + await stage.say(follow_up, on_event=on_event) + return load(destination) diff --git a/harness/src/agent_harness/world/__init__.py b/harness/src/agent_harness/world/__init__.py new file mode 100644 index 0000000..9fc85c9 --- /dev/null +++ b/harness/src/agent_harness/world/__init__.py @@ -0,0 +1,33 @@ +"""Generated worlds: a real data store behind an agent's tools. + +The pieces here are the parts that must be exact, so that what gets generated per agent stays +small: the runtime a world executes on, the snapshot every scenario restores from, and the probe +suite that decides whether a world is usable at all. +""" + +from .kinds import WorldKind, register_kind, supported as supported_kinds +from .probe import EDGE, HAPPY, SEQUENCE, ProbeReport, ProbeResult, dirty_state, probe +from .runtime import Call, Db, GeneratedWorld, ToolError, WorldSpec +from .snapshot import apply_overlay, read_manifest, restore, save + +__all__ = [ + "Call", + "Db", + "EDGE", + "GeneratedWorld", + "HAPPY", + "ProbeReport", + "ProbeResult", + "WorldKind", + "dirty_state", + "register_kind", + "supported_kinds", + "SEQUENCE", + "ToolError", + "WorldSpec", + "apply_overlay", + "probe", + "read_manifest", + "restore", + "save", +] diff --git a/harness/src/agent_harness/world/expectations.py b/harness/src/agent_harness/world/expectations.py new file mode 100644 index 0000000..c1b6b7d --- /dev/null +++ b/harness/src/agent_harness/world/expectations.py @@ -0,0 +1,91 @@ +"""What a world is expected to look like afterwards, and whether it does. + +Written once and used twice. The build stage declares a sequence and asserts the state it leaves +behind; a scenario declares the state a conversation should leave behind. Those are the same +question asked at two different scales, and if each had its own implementation they would drift +until a check that passes the gate fails the run for reasons that have nothing to do with the +agent. + +The shape is ``{"table.count": 3, "table.column": "value"}``: how many records there are, and +whether a particular value is among them. +""" + +from __future__ import annotations + +from typing import Any, Mapping + +COUNT = "count" + + +def check_state( + state: Mapping[str, list[dict[str, Any]]], expected: Mapping[str, Any] +) -> list[str]: + """Every expectation that does not hold, said in terms of what was found instead.""" + failures: list[str] = [] + for path, want in (expected or {}).items(): + table, _, column = str(path).partition(".") + if table not in state: + failures.append( + f"{path}: no {table} in this world; it has " + f"{', '.join(sorted(state)) or 'nothing'}" + ) + continue + rows = state[table] + if column in ("", COUNT): + if len(rows) != want: + failures.append(f"{path}: {len(rows)} rows, expected {want}") + continue + if rows and column not in rows[0]: + failures.append( + f"{path}: {table} has no {column}; its columns are " + f"{', '.join(sorted(rows[0]))}" + ) + continue + present = {str(row.get(column)) for row in rows} + # A list means every one of these has to be somewhere, which is how an expectation about + # a basket of several items is naturally written. Compared as a single value it could + # never hold, and an expectation that cannot hold grades nothing while appearing to. + wanted = list(want) if isinstance(want, (list, tuple)) else [want] + absent = [value for value in wanted if str(value) not in present] + if absent: + found = ", ".join(sorted(present)[:6]) or "nothing" + failures.append( + f"{path}: no row has {column}=" + + " or ".join(repr(value) for value in absent) + + f"; found {found}" + ) + return failures + + +def unresolvable( + state: Mapping[str, list[dict[str, Any]]], expected: Mapping[str, Any] +) -> list[str]: + """Expectations that name a table or column the world does not have. + + Separate from whether they hold, because they are a different kind of wrong. An expectation + that fails is a finding about the agent; one that names a table nobody built is a finding + about the expectation, and letting it through means grading a run against a typo. + """ + problems: list[str] = [] + for path in expected or {}: + table, _, column = str(path).partition(".") + if table not in state: + # Indexing a particular row is the most common way to write an expectation this + # cannot carry, and saying only "no such table" sends the reader looking for a + # spelling mistake instead of at the shape. + indexed = "[" in table + problems.append( + f"{path}: no table called {table!r}" + + ( + ". Expectations are about the whole table, not one row: use " + "'table.count' for how many, or 'table.column' for a value that has to " + "appear in some row." + if indexed + else "" + ) + ) + elif ( + column not in ("", COUNT) and state[table] and column not in state[table][0] + ): + problems.append(f"{path}: {table} has no column {column!r}") + return problems diff --git a/harness/src/agent_harness/world/kinds.py b/harness/src/agent_harness/world/kinds.py new file mode 100644 index 0000000..8d62a20 --- /dev/null +++ b/harness/src/agent_harness/world/kinds.py @@ -0,0 +1,196 @@ +"""What a kind of world has to be able to do, so the checks do not care which kind it is. + +A world backed by a database and a world backed by a page are different in every detail and the +same in what matters: something either exists in them or does not, an action either takes effect +or is refused, and what an action leaves behind is either carried or lost. Those are the things +worth checking, and none of them mention a table. + +So the checks are written against this, and a kind supplies the four answers only it can give: +what exists, what the mutable state is, how to freeze it, and how to put it back. Adding a kind +is a class and a registration; nothing in the gate changes. +""" + +from __future__ import annotations + +from typing import Any, Callable, Mapping, Protocol, runtime_checkable + +from .runtime import GeneratedWorld + + +def _rows(collection: Any) -> list[Any]: + """One collection's members, whichever shape it is kept in. + + A table gives a list of row mappings. A collection the agent's own code owns is as often a + mapping keyed by identifier, and iterating that yields keys rather than records, which is how + a check written for one shape silently reads the other. + """ + if isinstance(collection, dict): + return list(collection.values()) + if isinstance(collection, (list, tuple)): + return list(collection) + return [collection] + + +def _identifiers(state: Mapping[str, Any]) -> set[str]: + found: set[str] = set() + for name, collection in state.items(): + if isinstance(collection, dict): + # The keys of a mapping are identifiers in their own right, and usually the ones a + # tool is called with. + found.update(str(key) for key in collection if isinstance(key, str) and key) + for row in _rows(collection): + if isinstance(row, Mapping): + found.update( + value for value in row.values() if isinstance(value, str) and value + ) + elif isinstance(row, str) and row: + found.add(row) + return found + + +def _sizes(state: Mapping[str, Any]) -> dict[str, int]: + return {name: len(_rows(collection)) for name, collection in state.items()} + + +@runtime_checkable +class WorldKind(Protocol): + """The per-kind half of a world. The shared half is ``GeneratedWorld``.""" + + key: str + label: str + + def values_present(self, world: GeneratedWorld) -> set[str]: + """Every identifier this world contains. + + Answers whether the catalogue is complete: a contract that permits a value the world has + never heard of produces a tool that refuses forever, which is indistinguishable from a + tool being correctly strict. + """ + + def mutable_state(self, world: GeneratedWorld) -> dict[str, int]: + """Named parts of the world that an action can change, and how much is in each. + + Used for two things: noticing that a saved world still holds whatever the builder was + experimenting with, and noticing that a sequence of actions left nothing behind. + """ + + def describe(self, world: GeneratedWorld) -> str: + """A short human-readable account of what is in the world.""" + + +class SqliteWorld: + """A world whose state is rows in tables. Tool APIs, and anything with a data store.""" + + key = "sqlite" + label = "a database behind the agent's tools" + + def values_present(self, world: GeneratedWorld) -> set[str]: + return _identifiers(world.state()) + + def mutable_state(self, world: GeneratedWorld) -> dict[str, int]: + return _sizes(world.state()) + + def describe(self, world: GeneratedWorld) -> str: + counts = self.mutable_state(world) + return ", ".join(f"{name}: {count}" for name, count in sorted(counts.items())) + + +class BrowserWorld: + """A world whose state is pages and the actions that change them. + + ALK already carries a browser environment fed DOM snapshots and action fixtures, and it + already refuses a click matching no fixture. So this is the same move as the database kind: + generate instances of a shape that exists, rather than invent a mechanism. + + What exists here is the set of things an agent can reach, which is selectors and URLs rather + than ids; what changes is which snapshot is current and what the actions have mutated. + """ + + key = "browser" + label = "pages and the actions that change them" + + def values_present(self, world: GeneratedWorld) -> set[str]: + present: set[str] = set() + for collection in world.state().values(): + for row in _rows(collection): + if not isinstance(row, Mapping): + continue + for column in ("url", "selector", "id", "name", "action"): + value = row.get(column) + if isinstance(value, str) and value: + present.add(value) + return present + + def mutable_state(self, world: GeneratedWorld) -> dict[str, int]: + return _sizes(world.state()) + + def describe(self, world: GeneratedWorld) -> str: + counts = self.mutable_state(world) + return ", ".join(f"{name}: {count}" for name, count in sorted(counts.items())) + + +class InProcessWorld: + """A world whose state the agent's own code holds, rather than a store we stood up. + + This is what an adopted world usually is: the agent's tools were written to act on a structure + they build themselves, so the world holds that structure and does not interpret it. + """ + + key = "in_process" + label = "state the agent's own code keeps" + + def values_present(self, world: GeneratedWorld) -> set[str]: + return _identifiers(world.state()) + + def mutable_state(self, world: GeneratedWorld) -> dict[str, int]: + return _sizes(world.state()) + + def describe(self, world: GeneratedWorld) -> str: + counts = self.mutable_state(world) + return ", ".join(f"{name}: {count}" for name, count in sorted(counts.items())) + + +_REGISTRY: dict[str, Callable[[], WorldKind]] = { + SqliteWorld.key: SqliteWorld, + BrowserWorld.key: BrowserWorld, + InProcessWorld.key: InProcessWorld, +} + + +def register_kind(key: str, factory: Callable[[], WorldKind]) -> None: + """Add a kind of world. Computer use, a filesystem, a queue: a class and this line.""" + _REGISTRY[key] = factory + + +def resolve(key: str) -> WorldKind: + if key not in _REGISTRY: + raise NotImplementedError( + f"no world kind {key!r}; registered kinds are {', '.join(sorted(_REGISTRY))}" + ) + return _REGISTRY[key]() + + +def supported() -> tuple[str, ...]: + return tuple(sorted(_REGISTRY)) + + +def for_contract(contract: Any) -> WorldKind: + """The kind of world an agent needs, from what the contract says it is. + + Chosen rather than guessed at build time: an agent reachable by voice and by browser is one + agent with two runtimes, and which world to build is a decision about what is being tested. + """ + # What the store is, when the contract knows. That is the honest source: how a person reaches + # the agent says nothing about what its tools read and write, and an agent whose state lives + # in its own process is not a database however it is spoken to. + store = getattr(contract, "data_store", None) + named = str(getattr(store, "kind", "") or "").lower() + if named in _REGISTRY: + return resolve(named) + if named in ("in_process", "memory", "in-memory", "none", ""): + if named: + return resolve("in_process") + modality = str(getattr(contract, "modality", "") or "").lower() + if modality in ("browser", "computer_use", "cua"): + return resolve("browser") + return resolve("sqlite") diff --git a/harness/src/agent_harness/world/mutate.py b/harness/src/agent_harness/world/mutate.py new file mode 100644 index 0000000..636a1c0 --- /dev/null +++ b/harness/src/agent_harness/world/mutate.py @@ -0,0 +1,142 @@ +"""Breaking a world on purpose, to find out whether its checks would notice. + +The checks that verify an environment are written by whoever built it. That is the right way +round: what makes a world usable is a judgement about this agent, and no fixed set of probes +written in advance can make it for every agent. But it leaves nothing independent confirming the +checks work, and a check that cannot fail reports a healthy world forever. + +So the checks are put to a test they cannot talk their way out of. The world is damaged in ways +that are obviously wrong, and the checks have to go red. One that stays green through every +damaged world is not verifying anything, whatever it claims to inspect. + +The damage is deliberately generic, because a mutation that needed to understand the agent would +need the same judgement the checks needed, and nothing would be gained: + +- **emptied**: every collection loses its contents. A world with no data at all. +- **silenced**: every tool answers with nothing. Calls succeed and change nothing. + +Any check worth keeping fails against at least one of those. Most fail against both. +""" + +from __future__ import annotations + +from typing import Any, Callable + +from .runtime import GeneratedWorld + +EMPTIED = "emptied" +# Not a kind of damage: how the report says the damage itself did not happen. +UNDAMAGED = "could not be damaged" +SILENCED = "silenced" + +# What a silenced tool answers. Deliberately a plain empty string: it is the shape a handler +# returns when it has done nothing, which is exactly the failure being simulated. +_MUTE = "def handle(args, db):\n return ''\n" + + +def _empty(world: GeneratedWorld) -> None: + """Take the contents out of the world, leaving its shape intact. + + The store empties itself where it can, because only it knows what its engine needs: a + relational one has to suspend foreign keys, or deleting a referenced table fails and most of + the data stays. Dropping collection by collection through the world's own vocabulary is the + fallback, for a store that has no opinion. + """ + emptied = getattr(world.store, "clear", None) + if callable(emptied): + emptied() + else: + for name in list(world.state()): + try: + world.drop(name) + except Exception: + # One collection that will not empty is not a reason to abandon the mutation. What + # survives is reported by `left`, so the gate can tell a check that failed to + # notice from a mutation that never happened. + continue + # The agent's own state, where its code keeps what its tools act on. A world can have both. + held = world.state_object + if isinstance(held, dict): + for name, group in held.items(): + if isinstance(group, dict): + group.clear() + elif isinstance(group, list): + group.clear() + else: + held[name] = None + + +def left(world: GeneratedWorld) -> dict[str, int]: + """What is still in the world after it was supposed to be empty. + + The gate accuses a check of verifying nothing when it stays green through damage. That + accusation is only fair if the damage actually happened: a store that quietly refused to + empty leaves every check reading real data and looking vacuous, and the person then rewrites + a check that was right all along. + """ + return {name: len(rows) for name, rows in world.state().items() if rows} + + +def _silence(world: GeneratedWorld) -> None: + """Leave every tool answering with nothing, so no call has any effect.""" + for name in list(world.handlers): + world.handlers[name] = _MUTE + + +def damage() -> dict[str, Callable[[GeneratedWorld], None]]: + """Every way a world is broken on purpose, by name.""" + return {EMPTIED: _empty, SILENCED: _silence} + + +def unnoticed( + world_root: Any, + checks: list[tuple[str, str]], + *, + run: Callable[[str, GeneratedWorld], Any], + restore: Callable[[Any], GeneratedWorld], +) -> dict[str, list[str]]: + """Which checks fail to notice each kind of damage. + + Every mutation runs against its own restored copy, so one cannot inherit another's damage and + a check is never blamed for a world some earlier mutation had already emptied. + + Returns damage name to the checks that stayed green through it. A check appearing under every + kind of damage is one that cannot fail. + """ + survived: dict[str, list[str]] = {} + for name, apply in damage().items(): + broken = restore(world_root) + try: + apply(broken) + if name == EMPTIED: + remaining = left(broken) + if remaining: + # The mutation did not land, so nothing can be concluded from it. Saying so is + # the point: reporting these checks as blind would have somebody rewrite a + # check that was reading the world correctly the whole time. + survived[name] = [] + survived.setdefault(UNDAMAGED, []).append( + f"the world would not empty: {remaining}" + ) + continue + still_green = [] + for check_name, source in checks: + outcome = run(source, broken) + # A check that raises has not verified anything either, but that is a broken + # check rather than a blind one, and it is reported separately by the caller. + if getattr(outcome, "held", False): + still_green.append(check_name) + survived[name] = still_green + finally: + broken.close() + return survived + + +def blind(survived: dict[str, list[str]]) -> list[str]: + """Checks that stayed green through every kind of damage.""" + if not survived: + return [] + kinds = [names for kind, names in survived.items() if kind != UNDAMAGED] + if not kinds: + return [] + return sorted(set(kinds[0]).intersection(*kinds[1:])) if len(kinds) > 1 else sorted(kinds[0]) diff --git a/harness/src/agent_harness/world/probe.py b/harness/src/agent_harness/world/probe.py new file mode 100644 index 0000000..baa5498 --- /dev/null +++ b/harness/src/agent_harness/world/probe.py @@ -0,0 +1,400 @@ +"""Whether a generated world is usable, decided by exercising it. + +Published work on synthesised environments is consistent about two things. Most generated +environments contain bugs, so the gate has to aim at the ones that block rather than at +perfection. And the bugs cluster: edge-case handling first, then state consistency across +several calls. A gate that runs each handler once and calls it done misses both clusters. + +So this exercises every tool three ways, and then exercises the world as a sequence: + +- **happy**: a valid call, built from the values the contract says the argument accepts +- **edge**: an identifier that does not exist, and a required argument left out +- **sequence**: a declared series of calls whose final state is asserted + +The distinction that matters throughout is **refusal versus crash**. A tool that rejects a +nonexistent id is working: that refusal is the entire point of a real world. A tool that raises +``KeyError`` on the same input is broken. They are both failures to a naive check and opposite +outcomes here. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import Any, Iterable, Mapping, Sequence + +from ..contract import AgentContract, ToolSpec +from .expectations import check_state +from .kinds import WorldKind, for_contract +from .kinds import resolve as _resolve_kind +from .runtime import GeneratedWorld + +HAPPY = "happy" +EDGE = "edge" +SEQUENCE = "sequence" +COVERAGE = "coverage" +DATA = "data" + +# A value no generated world should ever have seeded, used to prove a lookup refuses. +ABSENT = "__does_not_exist__" + + +@dataclass +class ProbeResult: + name: str + kind: str + passed: bool + detail: str = "" + + +@dataclass +class ProbeReport: + results: list[ProbeResult] = field(default_factory=list) + + @property + def score(self) -> float: + return ( + sum(1 for result in self.results if result.passed) / len(self.results) + if self.results + else 0.0 + ) + + @property + def failures(self) -> list[ProbeResult]: + return [result for result in self.results if not result.passed] + + def summary(self) -> str: + if not self.results: + return "no probes ran" + lines = [ + f"{len(self.results) - len(self.failures)}/{len(self.results)} probes passed" + ] + for failure in self.failures: + lines.append(f" {failure.kind}:{failure.name}: {failure.detail}") + return "\n".join(lines) + + +def _valid_arguments(tool: ToolSpec) -> dict[str, Any]: + """A plausible call, using the values the contract says each argument accepts.""" + arguments: dict[str, Any] = {} + for arg in tool.args: + options = tool.arg_values.get(arg) + if isinstance(options, (list, tuple)): + usable = [value for value in options if value not in (None, "null", "")] + if usable: + arguments[arg] = usable[0] + continue + declared = tool.arg_types.get(arg, "") + if "list" in declared: + arguments[arg] = [] + elif "int" in declared: + arguments[arg] = 1 + elif "bool" in declared: + arguments[arg] = True + else: + arguments[arg] = ABSENT + return arguments + + +def _is_a_real_identifier(value: Any) -> bool: + """Whether a permitted value names a record, rather than being an enum like 'M' or 'null'.""" + if not isinstance(value, str) or value in ("", "null", "none", "None"): + return False + return len(value) > 2 and not value.isdigit() + + +def _missing_catalogue( + world: GeneratedWorld, contract: AgentContract, kind: WorldKind +) -> list[str]: + """Identifiers the contract says a tool accepts that are nowhere in the seeded world. + + The gap this closes is a whole category left unseeded. Every call naming a sauce then fails, + which looks from the outside exactly like a world being correctly strict, and a suite where + nothing can be ordered scores perfectly. Whether the catalogue is complete cannot be settled + by behaviour, so it is checked against the data. + """ + present = kind.values_present(world) + missing: list[str] = [] + for tool in contract.tools: + for arg, values in (tool.arg_values or {}).items(): + if not isinstance(values, (list, tuple)): + continue + if not _looks_like_an_identifier(arg, tool.arg_types.get(arg, "")): + continue + absent = [ + value + for value in values + if _is_a_real_identifier(value) and value not in present + ] + if absent: + shown = ", ".join(absent[:4]) + ( + f" and {len(absent) - 4} more" if len(absent) > 4 else "" + ) + missing.append(f"{tool.name}.{arg}: {shown}") + return missing + + +def _missing_argument(error: str) -> bool: + """Whether a failure is the language rejecting a call for want of a required argument.""" + said = (error or "").lower() + return "typeerror" in said and "argument" in said and ( + "missing" in said or "required" in said or "unexpected keyword" in said + ) + + +def _reads_argument(source: str, name: str) -> bool: + """Whether a handler actually takes this argument out of ``args``. + + Looking for the bare name is not enough. A handler that reads ``args['order_ids']`` and then + loops ``for order_id in order_ids`` mentions ``order_id`` all over itself while never reading + the argument the tool is given, so it silently ignores its input and reports success or + refuses everything. Both look fine from the outside, which is why this is checked at the + point of access rather than by behaviour. + """ + pattern = ( + rf"args\s*(?:\[\s*|\.get\s*\(\s*|\.pop\s*\(\s*)" + rf"['\"]{re.escape(name)}['\"]" + ) + return re.search(pattern, source) is not None + + +def _looks_like_an_identifier(name: str, _declared: str = "") -> bool: + """Whether an argument names a record that has to exist for the call to make sense. + + Decided by the name alone. Treating every ``str`` argument as a catalogue was a trap: a + ``size`` accepting "Medium" and "Large" then demanded rows called Medium and Large in the + world, which can never be seeded sensibly. The only ways out were to invent nonsense rows or + to edit the contract, so a check meant to catch a missing menu instead pushed towards + corrupting the record of what the agent is. + + A missed catalogue is a check that does not fire. A false one is a stage with no legal move, + which is much worse, so this stays narrow. + """ + return ( + name.endswith(("_id", "_ids", "_key", "_ref", "_code", "_sku")) or name == "id" + ) + + +def _identifier_arguments(tool: ToolSpec) -> dict[str, Any] | None: + """The same call with every identifier replaced by one that cannot exist. + + Deliberately not gated on the contract listing that argument's values. A contract that + failed to record them is exactly the case where nobody has checked what this tool does with + a bad id, so skipping the probe there drops it precisely where it is most needed. + """ + arguments = _valid_arguments(tool) + swapped = False + for arg in tool.args: + declared = tool.arg_types.get(arg, "") + if not tool.arg_values.get(arg) and not _looks_like_an_identifier( + arg, declared + ): + continue + arguments[arg] = [ABSENT] if "list" in declared.lower() else ABSENT + swapped = True + return arguments if swapped else None + + +def probe( + world: GeneratedWorld, + contract: AgentContract, + *, + sequences: Iterable[Mapping[str, Any]] = (), + kind: WorldKind | None = None, +) -> ProbeReport: + """Exercise the world and report what it can and cannot do. + + ``sequences`` are declared by whoever built the world, because knowing that adding an item + should make it appear in a listing is judgement about this agent, not something derivable + from a schema. + """ + report = ProbeReport() + kind = kind or for_contract(contract) + + # Every probe runs from the same starting world. Probes mutate, so without reverting + # between them each one inherits the debris of the last and a check expecting three rows + # finds seven. That is a fault in the harness, not in the world being checked. + baseline = world.checkpoint() + + for tool in contract.tools: + if tool.name not in world.handlers: + report.results.append( + ProbeResult(tool.name, COVERAGE, False, "contract tool has no handler") + ) + for name in world.handlers: + if name not in contract.tool_names(): + report.results.append( + ProbeResult( + name, COVERAGE, False, "handler for a tool the agent does not have" + ) + ) + + for gap in _missing_catalogue(world, contract, kind): + report.results.append( + ProbeResult( + gap.split(":")[0], + DATA, + False, + f"the contract accepts values the world does not have: {gap}", + ) + ) + if not _missing_catalogue(world, contract, kind): + report.results.append( + ProbeResult("catalogue", DATA, True, "every permitted identifier exists") + ) + + for tool in contract.tools: + if tool.name not in world.handlers: + continue + source = world.handlers[tool.name] + # Reading the source only says anything about a handler written here. A tool bound to the + # agent's own code has a handler that forwards every argument on, so it never names any of + # them, and checking for the names would fail every adopted tool while telling nobody + # anything. The names are the agent's own problem there, and its own code is what runs. + if not contract.adoptable(tool.name): + unread = [arg for arg in tool.args if not _reads_argument(source, arg)] + report.results.append( + ProbeResult( + tool.name, + COVERAGE, + not unread, + # A handler reading order_ids when the tool takes order_id refuses + # everything, which looks exactly like a handler correctly refusing a bad id. + # Behaviour alone cannot tell those apart, so the names are checked directly. + "" + if not unread + else f"never reads {', '.join(unread)}, which the contract says it takes", + ) + ) + + world.revert(baseline) + call = world.call(tool.name, _valid_arguments(tool)) + # A refusal here is acceptable: the contract's first listed value may genuinely be + # invalid in the seeded world. A crash never is. + report.results.append( + ProbeResult( + tool.name, + HAPPY, + call.ok or call.refused, + "" if call.ok or call.refused else call.error, + ) + ) + + bogus = _identifier_arguments(tool) + if bogus is not None: + world.revert(baseline) + call = world.call(tool.name, bogus) + report.results.append( + ProbeResult( + tool.name, + EDGE, + call.refused, + "" + if call.refused + else ( + "succeeded on an id that does not exist" + if call.ok + else f"crashed instead of refusing: {call.error}" + ), + ) + ) + + if tool.args: + world.revert(baseline) + missing = _valid_arguments(tool) + missing.pop(tool.args[0], None) + call = world.call(tool.name, missing) + # A tool bound to the agent's own code is a function with real parameters, so leaving a + # required one out is rejected by the language before the body runs. That is the call + # being refused, not the world falling over, and counting it as a crash would fail + # every adopted tool for behaving exactly as the agent's own runtime makes it behave. + declined = call.refused or ( + contract.adoptable(tool.name) and _missing_argument(call.error) + ) + report.results.append( + ProbeResult( + f"{tool.name}:without-{tool.args[0]}", + EDGE, + declined, + "" + if declined + else ( + "accepted a call with a required argument missing" + if call.ok + else f"crashed instead of refusing: {call.error}" + ), + ) + ) + + world.revert(baseline) + unknown = world.call(ABSENT, {}) + report.results.append( + ProbeResult( + "unknown-tool", + EDGE, + unknown.refused, + "" if unknown.refused else "an unknown tool did not refuse", + ) + ) + + for index, sequence in enumerate(sequences): + world.revert(baseline) + report.results.append(_run_sequence(world, sequence, index)) + + # Leave the world as the builder left it, not as the last probe left it. + world.revert(baseline) + return report + + +def dirty_state( + world: GeneratedWorld, + sequences: Iterable[Mapping[str, Any]], + kind: WorldKind | None = None, +) -> list[str]: + """Tables a scenario writes to that already hold rows before anything has happened. + + A world is the state every scenario starts from, so an order table with rows in it means + the builder's own testing was frozen into the base state. Every scenario then begins with + somebody else's order already in the cart, and a count check that should read one reads + seven. Which tables are transactional is not guessable from a schema, so it is worked out + by running the declared sequences and seeing what moves. + """ + kind = kind or _resolve_kind("sqlite") + baseline = world.checkpoint() + before = kind.mutable_state(world) + touched: set[str] = set() + for index, sequence in enumerate(sequences): + world.revert(baseline) + _run_sequence(world, sequence, index) + for name, size in kind.mutable_state(world).items(): + if size != before.get(name, 0): + touched.add(name) + world.revert(baseline) + return sorted(name for name in touched if before.get(name, 0) > 0) + + +def _run_sequence( + world: GeneratedWorld, sequence: Mapping[str, Any], index: int +) -> ProbeResult: + """Run a declared series of calls and check the state it leaves behind. + + This is the state-consistency check: the failure mode where each call works on its own and + the world still forgets what the previous one did. + """ + name = str(sequence.get("name") or f"sequence-{index}") + calls: Sequence[Mapping[str, Any]] = sequence.get("calls") or () + for step in calls: + call = world.call(str(step.get("tool", "")), step.get("arguments") or {}) + if step.get("expect") == "refusal": + if not call.refused: + return ProbeResult( + name, SEQUENCE, False, f"{call.name} should have refused" + ) + continue + if not call.ok: + return ProbeResult(name, SEQUENCE, False, f"{call.name}: {call.error}") + + failures = check_state(world.state(), sequence.get("expect_state") or {}) + if failures: + return ProbeResult(name, SEQUENCE, False, failures[0]) + return ProbeResult(name, SEQUENCE, True) diff --git a/harness/src/agent_harness/world/runtime.py b/harness/src/agent_harness/world/runtime.py new file mode 100644 index 0000000..5bcf8d9 --- /dev/null +++ b/harness/src/agent_harness/world/runtime.py @@ -0,0 +1,573 @@ +"""The runtime a generated world runs on. + +A generated world is a database plus one handler per tool. The handler decides what a call does; +this decides what a handler is allowed to be, what happens when one fails, and what the world +looks like afterwards. Keeping that here means a generated file stays small enough to read and +correct, and the parts that must be exact are not regenerated every time. + +The contract with the rest of the platform is ``EnvironmentAdapter``: ``reset`` publishes the +tools and the starting state, ``handle_tool_call`` executes one call, and the state afterwards is +what the checks grade. A world is therefore drivable by any loop that already drives an +environment, which is the whole reason we generate against this interface rather than inventing +one. +""" + +from __future__ import annotations + +import json +import sqlite3 +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Mapping, Sequence + +from ..environment import EnvironmentAdapter, EnvironmentSnapshot, ToolExecutionResult + + +class ToolError(Exception): + """A tool refusing for a real reason the agent should see and recover from. + + Distinct from a crash. A refusal is the world working: the id does not exist, the item is + unavailable, the argument is outside what the tool accepts. A crash is our bug, and the two + must never look the same to a caller deciding whether the agent behaved correctly. + """ + + +@dataclass +class Db: + """The handle a handler gets. Deliberately small: query, execute, one. + + Handlers get a database, not a filesystem and not a network. Anything a handler can reach is + something a generated world could depend on, and a world that depends on the outside is not + reproducible. + """ + + # Whatever this agent's records live in. A handler's statements are written in that store's + # own language, so this passes them through rather than interpreting them. + store: Any + # The agent's own state object, where its tools keep what they act on in memory rather than + # in a database. Their code is the thing that shapes it, so the world holds it and does not + # interpret it: freezing it is a serialisation, and restoring it is the reverse. + state: Any = None + + def query(self, sql: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]: + return self.store.query(sql, params) + + def one(self, sql: str, params: Sequence[Any] = ()) -> dict[str, Any] | None: + rows = self.query(sql, params) + return rows[0] if rows else None + + def execute(self, sql: str, params: Sequence[Any] = ()) -> int: + return self.store.execute(sql, params) + + # -- reading without a query language --------------------------------------------- + # + # Not every agent has a database. One whose state lives in services and files gets a world + # whose collections the harness invented, and there is no dialect to write a SELECT in. A + # handler that could only issue SQL would be unable to read the world it was given at all. + + def collections(self) -> list[str]: + """Every collection this world holds, by name.""" + return list(self.store.collections()) + + def records(self, collection: str) -> list[dict[str, Any]]: + """Every record in one collection. The store-agnostic way to read.""" + return list(self.store.records(collection)) + + def find(self, collection: str, **fields: Any) -> list[dict[str, Any]]: + """The records in a collection whose fields all match what was asked for.""" + return [ + record + for record in self.records(collection) + if all(record.get(field) == value for field, value in fields.items()) + ] + + def add(self, collection: str, record: Mapping[str, Any]) -> int: + return self.store.add(collection, record) + + +def settled(value: Any) -> Any: + """The value, with a coroutine run to completion first. + + A tool the agent wrote may well be async: every framework-decorated tool is. Handlers here + are synchronous, and the build stage is itself inside a running event loop, so ``asyncio.run`` + cannot be called directly. Running it on a worker thread gives it a loop of its own and keeps + the handler contract unchanged. + """ + import asyncio + import inspect + + if not inspect.isawaitable(value): + return value + from concurrent.futures import ThreadPoolExecutor + + with ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(asyncio.run, value).result() + + +def _is_refusal(raised: BaseException) -> bool: + """Whether an exception is the world saying no, rather than the world falling over. + + Matched by name as well as by identity. A generated handler often declares its own + ``ToolError`` rather than using the one already in scope, which is defensive and sensible + from where it sits, and would otherwise turn every deliberate refusal into a reported crash. + Relying on an invisible convention being followed is not a way to decide something this + load-bearing. + """ + if isinstance(raised, ToolError): + return True + return any(base.__name__ == "ToolError" for base in type(raised).__mro__) + + +@dataclass +class Call: + """One tool call and what the world did with it.""" + + name: str + arguments: dict[str, Any] + result: Any = None + ok: bool = True + error: str = "" + refused: bool = False + # When it happened, seconds since the epoch. What lets a recording and a list of calls be + # read as one thing: without it the UI can show what the agent did but not when, and "when" + # is the whole question for a spoken run. + at: float = 0.0 + + +class GeneratedWorld(EnvironmentAdapter): + """A database-backed world whose tools are generated per agent. + + Subclasses declare ``name``, ``tools`` and ``handlers``. Everything about execution, + refusal, and state reporting is here so that a generated subclass carries only the parts + that are specific to one agent. + """ + + name = "generated" + tools: list[dict[str, Any]] = [] + handlers: dict[str, str] = {} + + # Where the agent's own code lives, so a handler that binds to one of its tools can import + # it. Empty when the world implements the tools itself. + source_root: str = "" + # The agent's own in-memory state, for tools that take it as an argument instead of + # connecting to anything. Held opaquely: their code gives it shape. + state_object: Any = None + # How this agent says no in a returned value. A tool that answers "Error: no such order" is + # refusing, and recording that as a success would hide the very behaviour worth testing. + refusal_signature: str = "" + + def __init__( + self, database: str | Path = ":memory:", *, store: Any = None, kind: str = "" + ) -> None: + from .stores import open_store + + self.database = str(database) + # Where this agent's records live. Given rather than assumed, because the harness + # writes statements in whatever the agent's own store speaks and they have to reach + # it. A world with no store of its own gets one that says so. + self.store = store or open_store(kind or "sqlite", database=self.database) + self.calls: list[Call] = [] + + @property + def connection(self) -> Any: + """The store's own connection, where it has one. + + Kept so that code written when every world was a SQLite file still works. Anything + new should go through the store, or through put, change and drop, so it holds for a + world whose records are somewhere else. + """ + found = getattr(self.store, "connection", None) + if found is None: + raise AttributeError( + f"this world's store ({getattr(self.store, 'key', 'unknown')}) has no " + "connection. Use the store, or put, change and drop." + ) + return found + + def reach(self, source_root: str) -> None: + """Make the agent's own code importable, so a binding can call it rather than copy it. + + Two directories go on the path, not one. An agent pointed at flatly is imported from where + it sits, but an agent laid out as a package is nearly always pointed at the part under + test rather than at its root: `tau_bench/envs/retail` is where the agent is, while + `tau_bench.envs.retail.data` only resolves from the repository above it. Adding just the + directory named makes every import the agent's own code writes fail, which arrives as + "No module named tau_bench" and reads as the package being absent rather than as us + having pointed at the middle of it. + + The package root is found the way Python finds it: walk up while each directory is itself + a package, and stop at the first that is not. + """ + import sys + + self.source_root = str(source_root or "") + for path in self._import_roots(self.source_root): + if path not in sys.path: + sys.path.insert(0, path) + + @staticmethod + def _import_roots(source_root: str) -> list[str]: + """Where the agent's code can be imported from: where it sits, and its package root.""" + if not source_root: + return [] + roots = [source_root] + here = Path(source_root) + # Bounded by the filesystem root: `parents` stops there, so a source outside any package + # simply never enters the loop. + while (here / "__init__.py").exists() and here.parent != here: + here = here.parent + if str(here) not in roots: + roots.append(str(here)) + return roots + + # -- EnvironmentAdapter ---------------------------------------------------------- + + def reset(self, **_context: Any) -> EnvironmentSnapshot: + self.calls = [] + return EnvironmentSnapshot(tools=list(self.tools), state=self.state()) + + def observe(self, **_context: Any) -> EnvironmentSnapshot: + return EnvironmentSnapshot(tools=list(self.tools), state=self.state()) + + def handle_tool_call( + self, tool_call: Mapping[str, Any], **_context: Any + ) -> ToolExecutionResult | None: + name = str( + tool_call.get("name") or (tool_call.get("function") or {}).get("name") or "" + ) + call_id = tool_call.get("id") or tool_call.get("tool_call_id") + arguments = tool_call.get("arguments") or tool_call.get("args") or {} + if not isinstance(arguments, Mapping): + arguments = {} + + call = self.call(name, arguments) + content = ( + json.dumps(call.result, default=str) + if not isinstance(call.result, str) + else call.result + ) + return ToolExecutionResult( + tool_call_id=call_id, + tool_name=name or "unknown", + content=call.error if not call.ok else content, + result=call.result, + success=call.ok, + error=call.error or None, + state_updates=self.state(), + ) + + # -- execution ------------------------------------------------------------------- + + def call(self, name: str, arguments: Mapping[str, Any] | None = None) -> Call: + """Execute one call and record it. Never raises: a failure is an outcome, not an event. + + An unknown tool is a refusal rather than a silent success. An agent reaching for a tool + that does not exist is a finding, and answering it with an acknowledgement is how a test + passes something it should have caught. + """ + args = dict(arguments or {}) + if name not in self.handlers: + return self._record( + Call( + name=name, + arguments=args, + ok=False, + refused=True, + error=( + f"no such tool {name!r}; this agent has " + f"{', '.join(sorted(self.handlers)) or 'none'}" + ), + ) + ) + + namespace: dict[str, Any] = {"ToolError": ToolError, "json": json} + try: + exec(compile(self.handlers[name], f"", "exec"), namespace) + handle = namespace.get("handle") + if not callable(handle): + raise RuntimeError("handler defines no handle(args, db)") + value = handle(args, Db(self.store, self.state_object)) + except Exception as raised: + if _is_refusal(raised): + return self._record( + Call( + name=name, + arguments=args, + ok=False, + refused=True, + error=str(raised), + ) + ) + # Our bug, not the agent's. Labelled differently so a run is never scored + # against a world that fell over. + return self._record( + Call( + name=name, + arguments=args, + ok=False, + error=f"{type(raised).__name__}: {raised}", + ) + ) + # A tool of the agent's own may refuse by returning rather than by raising, which is + # ordinary in code that was never written to be tested. Recording that as a success + # would hide exactly the behaviour worth measuring, so the agent's own convention + # decides. Only the recording differs: the value still reaches the agent unchanged. + if self._refused_by_value(value): + return self._record( + Call( + name=name, + arguments=args, + result=value, + ok=False, + refused=True, + error=str(value)[:400], + ) + ) + return self._record(Call(name=name, arguments=args, result=value)) + + def _refused_by_value(self, value: Any) -> bool: + """Whether a returned value is this agent's way of saying no. + + The convention is recorded as a description, because that is what somebody reading the + agent's code can actually write: "strings starting with Error:". So the marker is taken + from inside it rather than treating the whole sentence as a prefix, which would match + nothing and quietly record every refusal as a success. + """ + if not isinstance(value, str) or not value: + return False + described = (self.refusal_signature or "").strip() + if not described: + return False + for marker in self._markers(described): + if value.lower().startswith(marker.lower()): + return True + return False + + def _markers(self, described: str) -> list[str]: + """The literal markers named inside a described convention. + + Anything quoted is taken as written, since that is how a convention gets spelled out. With + nothing quoted the whole description is treated as the marker, which is right when somebody + recorded just the prefix itself. + """ + import re + + # A convention written for people gets quoted the way people quote, and a model writing + # JSON often escapes those quotes. Left in, the backslash ends up inside the marker, so + # "Error:" is looked for as 'Error:\' and matches nothing at all. Every refusal is then + # recorded as a success, which is the failure this whole field exists to prevent. + plain = described.replace('\\"', '"').replace("\\'", "'") + quoted = re.findall(r"[\"'“”‘’`]([^\"'“”‘’`]{1,40})[\"'“”‘’`]", plain) + found = [one.strip().strip("\\").strip() for one in quoted] + # A convention that lists examples separates them, and the separator sits between one + # closing quote and the next opening one, so it is matched as though it were quoted too. + # A marker of "," would make any result beginning with a comma a refusal, so anything + # without a character a message could start with is dropped. + found = [one for one in found if any(char.isalnum() for char in one)] + return found or [plain.strip()] + + def _record(self, call: Call) -> Call: + # Stamped here rather than by the caller, so every call is stamped and none of them + # depend on whoever made it remembering to. + call.at = call.at or time.time() + self.calls.append(call) + return call + + # -- state ----------------------------------------------------------------------- + + def _settle(self) -> None: + """Close any transaction left open on the connection. + + A handler that only reads still leaves an implicit read transaction behind, and SQLite + refuses to back up into a connection that has one open: "destination database is in + use". Left unsettled, the first read-only handler poisons every probe after it, and the + world can never be checked or saved. + """ + connection = getattr(self.store, "connection", None) + if connection is None: + # Nothing to settle. A store with no transactions has no open one to close, and + # reaching for a connection it never had would fail every probe on such a world. + return + try: + connection.commit() + except sqlite3.Error: + connection.rollback() + + def checkpoint(self) -> Any: + """A copy of everything the world holds, to come back to. + + Probes and smoke calls mutate: ordering an item inserts a record, cancelling one changes + it. Without a way back, each runs against the debris of the ones before it, and a check + expecting three records finds seven. + + Both halves are copied, and that matters more for an adopted world than a generated one. + A tool the agent wrote changes the structure it was given, in place. Backing up only the + store would leave those changes permanent, so a smoke call against one record would quietly + spend it, and whatever ran later against that same record would fail for a reason nothing + could see. + """ + import copy as duplicate + + self._settle() + # Through the store's own freeze rather than a SQLite backup, so a world whose records + # live somewhere else is revertible too. Every store knows how to go back; only some of + # them have a connection to copy. + store = self.store.freeze() + held = duplicate.deepcopy(self.state_object) if self.state_object is not None else None + return {"store": store, "state": held} + + def revert(self, checkpoint: Any) -> None: + """Put everything back as it was when the checkpoint was taken.""" + import copy as duplicate + + self._settle() + # A bare connection is accepted so that anything written against the older shape of this + # method keeps working rather than reverting nothing at all, which would be silent. + if isinstance(checkpoint, sqlite3.Connection): + checkpoint.backup(self.connection) + return + held = (checkpoint or {}).get("store") + if held is not None: + self.store.restore(held) + if (checkpoint or {}).get("state") is not None: + self.state_object = duplicate.deepcopy(checkpoint["state"]) + + def state(self) -> dict[str, Any]: + """What the checks compare against after a run. + + Tables and their rows, plus whatever the agent's own tools keep in memory. A world that + adopted the agent's code may have all of its state in the second of those, so a check has + to be able to see both without knowing which kind of world it is grading. + """ + found: dict[str, Any] = { + name: self.store.records(name) for name in self.store.collections() + } + if isinstance(self.state_object, dict): + # Collections the agent's own code owns. Not merged blindly: a table and a key of + # the same name would silently shadow one another, and a check comparing the wrong + # one would be wrong in a way nobody could see. + for key, value in self.state_object.items(): + found.setdefault(str(key), value) + elif self.state_object is not None: + found.setdefault("state", self.state_object) + return found + + # -- changing the world, without naming what it is kept in ------------------------ + # + # A scenario changes the world before it runs, and it must not have to know whether the world + # is a database, a mapping the agent's own code owns, or something else again. Speaking SQL + # here would write SQLite into every scenario ever written, and the store is the one thing + # this design expects to vary per agent. + # + # So the vocabulary is collections and records, which every store has under some name, and + # each method dispatches on what the collection actually is. The preferred way to change the + # world is still the agent's own tools, because anything they refuse would have refused the + # agent too; these are for the states no tool can produce. + + def _table(self, collection: str) -> bool: + return bool(self.store.holds(collection)) + + def _held(self, collection: str) -> Any: + if isinstance(self.state_object, dict): + return self.state_object.get(collection) + return None + + def put(self, collection: str, record: Mapping[str, Any], *, key: str = "") -> None: + """Add one record to a collection, whatever the collection is kept in.""" + if self._table(collection): + self.store.add(collection, record) + return + held = self._held(collection) + if isinstance(held, dict): + if not key: + raise KeyError( + f"{collection} is keyed, so adding to it needs a key: " + "world.put(collection, record, key=...)" + ) + held[key] = dict(record) + return + if isinstance(held, list): + held.append(dict(record)) + return + # A collection nobody has created yet is made here rather than refused. An agent whose + # state lives in services and files has no store to declare tables in, so every collection + # the world needs is one the harness invents: refusing the first record leaves that agent + # with a world that cannot hold anything at all. + made = getattr(self.store, "start_collection", None) + if callable(made): + made(collection, keyed=bool(key)) + self.store.add(collection, {**record, "_id": key} if key else record) + return + raise KeyError(f"no collection called {collection!r}; this world has {sorted(self.state())}") + + def change(self, collection: str, key: str, changes: Mapping[str, Any], *, by: str = "") -> int: + """Change records in a collection. Returns how many were changed. + + ``by`` names the column a table is keyed on. A collection the agent's own code keeps is + keyed already, so it is not needed there. + """ + if self._table(collection): + return self.store.amend(collection, key, changes, by=by) + held = self._held(collection) + if isinstance(held, dict) and key in held: + if isinstance(held[key], dict): + held[key].update(dict(changes)) + else: + held[key] = dict(changes) + return 1 + raise KeyError(f"nothing called {key!r} in {collection!r}") + + def drop(self, collection: str, key: str = "", *, by: str = "") -> int: + """Remove a record, or the whole contents of a collection when no key is given.""" + if self._table(collection): + return self.store.remove(collection, key, by=by) + held = self._held(collection) + if isinstance(held, dict): + if not key: + count = len(held) + held.clear() + return count + return 1 if held.pop(key, None) is not None else 0 + if isinstance(held, list): + count = len(held) + del held[:] + return count + raise KeyError(f"no collection called {collection!r}; this world has {sorted(self.state())}") + + def shapes(self) -> str: + """What this world's collections actually are, in words. + + Said wherever code written against the wrong shape fails. A table gives a list of records; + a collection the agent's own code keeps is often a mapping keyed by identifier, and + iterating that yields strings. No amount of general advice substitutes for naming which is + which, for the world in front of whoever got it wrong. + """ + lines = [] + for name, held in sorted(self.state().items()): + if isinstance(held, dict): + first = next(iter(held), None) + lines.append( + f" {name}: a mapping of {len(held)} records keyed by identifier" + + (f", e.g. {first!r}" if first is not None else "") + + ". Iterate .values(), or .items() when the key matters." + ) + elif isinstance(held, list): + lines.append(f" {name}: a list of {len(held)} records. Iterate it directly.") + else: + lines.append(f" {name}: a single {type(held).__name__}.") + return "This world holds:\n" + ("\n".join(lines) or " nothing yet") + + def close(self) -> None: + self.store.close() + + +@dataclass +class WorldSpec: + """What a generated world is, before it is written out.""" + + agent: str + schema_sql: str = "" + tools: list[dict[str, Any]] = field(default_factory=list) + handlers: dict[str, str] = field(default_factory=dict) + notes: str = "" diff --git a/harness/src/agent_harness/world/snapshot.py b/harness/src/agent_harness/world/snapshot.py new file mode 100644 index 0000000..0feb2f3 --- /dev/null +++ b/harness/src/agent_harness/world/snapshot.py @@ -0,0 +1,236 @@ +"""Freezing a world, and starting every scenario from the same frozen copy. + +The database is built once and snapshotted; that snapshot is the base state. A scenario restores +its own copy and layers on whatever it additionally needs, so scenarios cannot inherit each +other's leftovers and a run is repeatable a week later. + +Which is why the overlay exists: a scenario that needs a customer with three open orders adds +those rows to a restored copy rather than editing the snapshot. The base world stays the shared +starting point instead of drifting toward whichever scenario was written last. +""" + +from __future__ import annotations + +import json +import shutil +import sqlite3 +from pathlib import Path +from typing import Any, Mapping + +from .runtime import GeneratedWorld + +DATABASE = "world.sqlite" +HANDLERS = "handlers" +MANIFEST = "manifest.json" +STATE = "state.json" + + +def saved(path: str | Path | None) -> bool: + """Whether a world has been written here. + + One function, because this question gets asked from six places: the build stage, the + conversation, the session listing, the CLI and the UI. Asked as "is there a world.sqlite" + each of those was really asking "is this a SQLite world", so an agent whose state lives in + services and files saved a world that scored 1.00 and was then invisible to all of them. + """ + return bool(path) and (Path(path) / MANIFEST).exists() + + +WORLD_MODULE = "world.py" + +_MODULE = '''"""Generated world for {agent}. Do not edit by hand; regenerate instead. + +{notes} +""" + +from pathlib import Path + +from agent_harness.world.runtime import GeneratedWorld + +_HERE = Path(__file__).parent + +TOOLS = {tools} + + +class World(GeneratedWorld): + name = {agent!r} + tools = TOOLS + handlers = {{ + name: (_HERE / "handlers" / f"{{name}}.py").read_text(encoding="utf-8") + for name in {handler_names} + }} + + +def load(database=None): + """This world, restored from the snapshot beside this file. + + Through `restore` rather than by opening a database directly, because not every world has + one: an agent whose state lives in services and files keeps its records in the snapshot, and + naming a SQLite file would hand back an empty world instead of this one. + """ + from agent_harness.world.snapshot import restore + + return restore(_HERE, into=database) if database else restore(_HERE) +''' + + +def save( + world: GeneratedWorld, + path: str | Path, + *, + notes: str = "", + sequences: list[dict[str, Any]] | None = None, + world_checks: Mapping[str, str] | None = None, +) -> Path: + """Write the world out: the snapshot, the handlers, the module, and a manifest.""" + root = Path(path) + (root / HANDLERS).mkdir(parents=True, exist_ok=True) + + # Through the store, so a world whose records live somewhere other than a SQLite file, or + # nowhere at all, freezes by its own means rather than by one assumed here. + world.store.save_to(root) + + for name, source in world.handlers.items(): + (root / HANDLERS / f"{name}.py").write_text(source, encoding="utf-8") + + (root / WORLD_MODULE).write_text( + _MODULE.format( + agent=world.name, + notes=notes or "Generated from the agent's contract.", + tools=json.dumps(world.tools, indent=4), + handler_names=json.dumps(sorted(world.handlers)), + ), + encoding="utf-8", + ) + + # The agent's own in-memory state, where its tools keep what they act on there rather + # than in the database. Frozen as JSON so restoring is the exact reverse, and so a + # person can read what the world starts from. + if world.state_object is not None: + # Round-tripped rather than only written. Every scenario restores from this file, so state + # that does not survive the trip would come back subtly different and every check after + # it would be grading something else. Better to fail here than to be wrong quietly. + frozen = json.dumps(world.state_object, indent=2, default=str) + if json.loads(frozen) != world.state_object: + raise ValueError( + "the agent's state does not survive being frozen as JSON, so restoring it would " + "not give back what was saved. Every scenario starts from that restore, so this " + "world cannot be trusted. What is in the state that is not plain JSON?" + ) + (root / STATE).write_text(frozen, encoding="utf-8") + + state = world.state() + (root / MANIFEST).write_text( + json.dumps( + { + "agent": world.name, + # Which store this world used, so restoring it opens the same one rather + # than assuming a database that may never have existed. + "store": getattr(world.store, "key", "sqlite"), + "tools": sorted(world.handlers), + # Written because restore reads it. Without it a restored world publishes no + # tool descriptions at all, and every later stage has to reconstruct them. + "tool_specs": list(world.tools), + "tables": {name: len(rows) for name, rows in state.items()}, + # Kept because they are judgement about this agent, not something a schema + # implies. A world picked up again can be re-verified without redeclaring them. + "sequences": list(sequences or []), + # The world's own checks are judgement about this agent, so a world picked + # up again keeps them rather than having them rewritten from scratch. + "world_checks": dict(world_checks or {}), + # Where the agent's own code lives. Kept because a restored world has to + # be able to import the tools it was bound to, and a scenario run happens + # long after the build stage that found the path. + "source_root": world.source_root, + # How this agent says no in a returned value. Without it a restored world + # cannot tell a refusal from a success, so every run records "Error: no such + # order" as if the call worked, and a check asking whether the agent was + # refused is answered wrongly rather than reported as unanswerable. + "refusal_signature": world.refusal_signature, + "notes": notes, + }, + indent=2, + ensure_ascii=False, + ), + encoding="utf-8", + ) + return root + + +def restore(path: str | Path, *, into: str | Path | None = None) -> GeneratedWorld: + """A fresh, independent copy of the frozen world. + + In memory by default, because a scenario should not be able to write back into the snapshot + every later scenario depends on. + """ + root = Path(path) + source = root / DATABASE + if not (root / MANIFEST).exists(): + raise FileNotFoundError(f"no world snapshot at {root}") + + manifest = read_manifest(root) + handlers = { + name: (root / HANDLERS / f"{name}.py").read_text(encoding="utf-8") + for name in manifest.get("tools", []) + if (root / HANDLERS / f"{name}.py").exists() + } + + named = str(manifest.get("store") or "sqlite") + if into is None: + world = GeneratedWorld(":memory:", kind=named) + # Only where there is one. A world whose records the agent's own code keeps has no + # database file, and demanding one would make it unrestorable. + if source.exists() and getattr(world.store, "connection", None) is not None: + origin = sqlite3.connect(source) + with world.connection: + origin.backup(world.connection) + origin.close() + else: + # A store that keeps its records somewhere other than a SQLite file loads them its own + # way. Without this the world comes back with an empty store, and everything a check + # reads is whatever happened to land in the agent's state instead. + world.store.load_from(root) + else: + target = Path(into) + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + world = GeneratedWorld(target, kind=named) + + world.name = manifest.get("agent", "generated") + world.handlers = handlers + world.tools = manifest.get("tool_specs", []) + world.refusal_signature = str(manifest.get("refusal_signature") or "") + # A world whose handlers bind to the agent's own code cannot run them unless that code + # is importable again, and the frozen state is what those tools act on. + reached = str(manifest.get("source_root") or "") + if reached: + world.reach(reached) + frozen_state = root / STATE + if frozen_state.exists(): + world.state_object = json.loads(frozen_state.read_text(encoding="utf-8")) + return world + + +def read_manifest(path: str | Path) -> dict[str, Any]: + return json.loads((Path(path) / MANIFEST).read_text(encoding="utf-8")) + + +def apply_overlay(world: GeneratedWorld, overlay: Mapping[str, Any] | None) -> int: + """Layer one scenario's own rows onto a restored world. + + ``{"table": [{"column": value}, ...]}``. The only sanctioned way a scenario adds data, so the + base world stays the shared starting point rather than drifting per scenario. + """ + written = 0 + for table, rows in (overlay or {}).items(): + for row in rows or []: + if not isinstance(row, Mapping) or not row: + continue + columns = ", ".join(row) + marks = ", ".join("?" for _ in row) + world.connection.execute( + f"INSERT INTO {table} ({columns}) VALUES ({marks})", list(row.values()) + ) + written += 1 + world.connection.commit() + return written diff --git a/harness/src/agent_harness/world/stores/__init__.py b/harness/src/agent_harness/world/stores/__init__.py new file mode 100644 index 0000000..d97c278 --- /dev/null +++ b/harness/src/agent_harness/world/stores/__init__.py @@ -0,0 +1,282 @@ +"""The stores the harness can stand up for an agent, and what every one of them owes a world. + +A store is the thing underneath an agent's tools: whatever really holds the records its queries +run against. It is never asked to execute a tool. It is asked to exist, to hold data, to say what +it holds, to let a scenario change a little of it, and to go back to how it was. + +Which engine gets stood up is read off the agent, never chosen for it. Postgres and ClickHouse +disagree about dialect, types and what a transaction even means, so testing one against the other +grades an agent on queries it never runs. An engine the harness cannot stand up is an answer, not +a reason to substitute something that merely resembles it. + +What a store owes falls into four groups, and most stores care about three: + + lifecycle start, stop, dsn stand it up and say where it is + contents apply, execute, query statements, in whatever this engine speaks + records collections, holds, records, add, amend, remove + going back freeze, restore between scenarios + save_to, load_from to and from disk, for the base world + +The records group is what keeps a scenario from ever naming a store. `world.put`, `world.change` +and `world.drop` land here, so the same scenario runs against SQLite, against Postgres in a +container, or against a structure the agent's own code holds, without a line of it changing. +`state()` comes free from that group, and `Records` provides it. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Callable, Mapping, Protocol, Sequence, runtime_checkable + + +class StoreError(RuntimeError): + """The store could not be stood up, or could not answer. + + Distinct from anything the agent did. A store that will not start is our problem and should + stop the run loudly, because every result after it would be measured against something that + is not there. + """ + + +@dataclass +class Snapshot: + """Everything a store held at one moment, and what it takes to put it back. + + ``rows`` is kept in the shape ``state()`` reports, so a check written against a world's state + reads a snapshot without knowing which engine produced it. + + ``counters`` is whatever an engine hands out that is not itself a record: a Postgres sequence, + a MySQL auto-increment, anything that keeps counting after the rows are gone. Restoring rows + without restoring these gives the next scenario ids that continue from the last one, and a + check naming a specific id then fails for a reason that has nothing to do with the agent. + Engines that hand out nothing of the sort leave it empty, which is not a gap. + """ + + rows: dict[str, list[dict[str, Any]]] = field(default_factory=dict) + counters: dict[str, int] = field(default_factory=dict) + + def counts(self) -> dict[str, int]: + return {name: len(rows) for name, rows in self.rows.items()} + + +@runtime_checkable +class Store(Protocol): + """A running store the world's records live in.""" + + # What this engine is. ``key`` is the same thing under the name a saved manifest already + # uses, so a world written before this split still reopens. + engine: str + key: str + + def start(self) -> None: ... + def stop(self) -> None: ... + def dsn(self) -> str: ... + + # Statements the harness wrote, in whatever this store speaks. + def apply(self, script: str) -> None: ... + def execute(self, statement: str, params: Sequence[Any] = ()) -> int: ... + def query(self, statement: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]: ... + + # What a scenario and its checks need without writing a statement themselves. + def collections(self) -> list[str]: ... + def holds(self, collection: str) -> bool: ... + def records(self, collection: str) -> list[dict[str, Any]]: ... + def state(self) -> dict[str, list[dict[str, Any]]]: ... + def add(self, collection: str, record: Mapping[str, Any]) -> int: ... + def amend( + self, collection: str, key: str, changes: Mapping[str, Any], *, by: str = "" + ) -> int: ... + def remove(self, collection: str, key: str = "", *, by: str = "") -> int: ... + + # Between scenarios, in memory. + def freeze(self) -> Snapshot: ... + def restore(self, snapshot: Snapshot) -> None: ... + + # To and from disk, so the base world outlives the process that built it. + def save_to(self, path: str | Path) -> None: ... + def load_from(self, path: str | Path) -> None: ... + + def close(self) -> None: ... + + +class Records: + """``state`` from the record methods, for any store that has them. + + Kept in one place because the two would otherwise drift, and they are the pair the gates + compare: the bite gate empties a store and reads ``state``, while a scenario changes it + through ``add`` and ``amend``. If those disagree about what a collection contains, a check + passes against something no scenario can produce. + """ + + def state(self) -> dict[str, list[dict[str, Any]]]: + return {name: self.records(name) for name in self.collections()} # type: ignore[attr-defined] + + +class Held: + """The record methods, and disk, for a store that already answers ``state``. + + The mirror of ``Records``, for stores built the other way round: a container store reads + everything it holds in one go, and the per-collection questions follow from that. Saving to + disk is the snapshot as JSON, which works for any engine because a snapshot is already the + engine-independent shape. + + ``add``, ``amend`` and ``remove`` are not derivable and are left to the engine. A store + without them refuses loudly rather than silently doing nothing, because the alternative is a + scenario whose setup appears to run and changes nothing, and a run then graded against a + world that was never set up. + """ + + engine: str = "" + + @property + def key(self) -> str: + return self.engine + + def collections(self) -> list[str]: + return sorted(self.state()) # type: ignore[attr-defined] + + def holds(self, collection: str) -> bool: + return collection in self.state() # type: ignore[attr-defined] + + def records(self, collection: str) -> list[dict[str, Any]]: + return self.state().get(collection, []) # type: ignore[attr-defined] + + def execute(self, statement: str, params: Sequence[Any] = ()) -> int: + self.apply(statement) # type: ignore[attr-defined] + return 0 + + def query(self, statement: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]: + raise StoreError( + f"{self.engine} does not read back arbitrary statements. Read what it holds with " + "records() or state()." + ) + + def add(self, collection: str, record: Mapping[str, Any]) -> int: + raise StoreError(_UNWRITABLE.format(engine=self.engine, verb="add to")) + + def amend( + self, collection: str, key: str, changes: Mapping[str, Any], *, by: str = "" + ) -> int: + raise StoreError(_UNWRITABLE.format(engine=self.engine, verb="change")) + + def remove(self, collection: str, key: str = "", *, by: str = "") -> int: + raise StoreError(_UNWRITABLE.format(engine=self.engine, verb="remove from")) + + def clear(self) -> None: + """Empty it, by restoring a snapshot that holds nothing.""" + self.restore(Snapshot()) # type: ignore[attr-defined] + + def save_to(self, path: str | Path) -> None: + import json + + root = Path(path) + root.mkdir(parents=True, exist_ok=True) + frozen = self.freeze() # type: ignore[attr-defined] + (root / SAVED).write_text( + json.dumps({"rows": frozen.rows, "counters": frozen.counters}, indent=2, default=str), + encoding="utf-8", + ) + + def load_from(self, path: str | Path) -> None: + import json + + held = Path(path) / SAVED + if not held.exists(): + raise StoreError(f"no saved store at {held}") + kept = json.loads(held.read_text(encoding="utf-8")) + self.restore(Snapshot(rows=kept.get("rows") or {}, counters=kept.get("counters") or {})) # type: ignore[attr-defined] + + def close(self) -> None: + self.stop() # type: ignore[attr-defined] + + +# What a saved container store is written as. Not the engine's own dump format: a snapshot is +# already engine-independent, and a dump would tie the saved world to the version that wrote it. +SAVED = "store.json" + +_UNWRITABLE = ( + "{engine} has no way to {verb} a collection one record at a time, so a scenario cannot set " + "up on it. Give the store add, amend and remove in this engine's own language." +) + +_REGISTRY: dict[str, Callable[..., Store]] = {} + +# Names people and manifests actually write, pointing at the engine they mean. Kept explicit +# rather than normalised in code, because guessing which engine an unrecognised word meant is +# how an agent ends up graded against the wrong one. +_ALIASES = { + "": "in_process", + "none": "in_process", + "memory": "in_process", + "in-memory": "in_process", + "inprocess": "in_process", +} + + +def register_store(engine: str, factory: Callable[..., Store]) -> None: + """Teach the harness an engine. A class and this line. + + The cost of this line is what decides whether "whatever the agent uses" is real or an + aspiration, which is why the shared work lives in ``ContainerStore`` and an engine + contributes only what genuinely differs. + """ + _REGISTRY[engine] = factory + + +def supported() -> tuple[str, ...]: + return tuple(sorted(_REGISTRY)) + + +def resolve(engine: str = "", **options: Any) -> Store: + """The store for an engine, or a refusal naming what there is. + + Deliberately not a fallback. An agent on an engine nobody has taught the harness to run is a + gap worth reporting, and quietly handing it a different store would produce a green suite + about queries the agent never executes. + """ + named = (engine or "").strip().lower() + named = _ALIASES.get(named, named) + if named not in _REGISTRY: + raise StoreError( + f"no store for engine {named!r}; the harness can stand up " + f"{', '.join(supported()) or 'nothing yet'}. Adding one is a class with the record " + "methods and a call to register_store, or write_store_ops for an engine in a container." + ) + return _REGISTRY[named](**options) + + +# The name the rest of the harness has always called this by. +open_store = resolve + + +from .inprocess import InProcessStore # noqa: E402 +from .sqlite import SqliteStore # noqa: E402 + +register_store(SqliteStore.engine, SqliteStore) +register_store(InProcessStore.engine, InProcessStore) + +from .container import ContainerStore, docker, strays # noqa: E402 +from .postgres import PostgresStore # noqa: E402 + +# Postgres is registered as the worked example, not as the supported list. An engine the harness +# has never seen is meant to be written at build time against ``ContainerStore`` and proved by +# the gates, rather than waiting for someone to ship a class for it. +register_store(PostgresStore.engine, PostgresStore) + +__all__ = [ + "ContainerStore", + "InProcessStore", + "PostgresStore", + "Records", + "Snapshot", + "SqliteStore", + "Store", + "StoreError", + "docker", + "open_store", + "register_store", + "resolve", + "strays", + "supported", +] diff --git a/harness/src/agent_harness/world/stores/container.py b/harness/src/agent_harness/world/stores/container.py new file mode 100644 index 0000000..60b91a3 --- /dev/null +++ b/harness/src/agent_harness/world/stores/container.py @@ -0,0 +1,207 @@ +"""Standing an engine up in a container, which is the part no engine does differently. + +Pulling an image, giving it a free port, waiting for it to actually answer, tearing it down +and not leaking it when a run is killed: none of that is about Postgres. It is the same work +for MySQL, ClickHouse, Mongo or anything else the harness is ever asked to run, so it is +written once here. + +What an engine contributes is only what genuinely differs -- how to reach it, how to read what +it holds, and how to put that back. That is a small surface deliberately, because the cost of +teaching the harness a new engine is the thing that decides whether "whatever the agent uses" +is real or just an aspiration. +""" + +from __future__ import annotations + +import os +import secrets +import subprocess +import time + +from . import Held, StoreError + +# How long to wait for a fresh container to start answering. The first run on a machine pulls +# the image, which dominates; afterwards this is a second or two. +READY_TIMEOUT_SECONDS = 180.0 + +# Marks every container this module starts, so strays from a killed run can be found and +# removed without guessing at names. +LABEL = "alk.harness.store" + +# The network to join, when the harness is itself in a container. Publishing a port to the +# host's loopback is enough when the harness runs on the host, but from inside a container +# 127.0.0.1 is its own loopback and the engine is not there. Sharing a network instead lets +# the engine be reached by container name, on the port it actually listens on. +NETWORK = "ALK_DOCKER_NETWORK" + + +def docker(*args: str, check: bool = True) -> str: + """Run a docker command, and turn its failure into something worth reading.""" + try: + done = subprocess.run( # nosec B603: list args, never shell=True + ("docker", *args), capture_output=True, text=True, check=False + ) + except FileNotFoundError as exc: # pragma: no cover - depends on the machine + raise StoreError( + "docker is not on PATH, so no store can be stood up. Install Docker, or start " + "Colima, and try again." + ) from exc + if check and done.returncode != 0: + raise StoreError( + f"docker {' '.join(args)} failed ({done.returncode}): " + f"{(done.stderr or done.stdout).strip()}" + ) + return done.stdout.strip() + + +class ContainerStore(Held): + """An engine the harness runs in a container for the agent to be pointed at. + + Started once for a suite and reset between scenarios: standing an engine up costs seconds + and putting its data back costs milliseconds, so the container stays and only its contents + move. + + Subclasses supply ``image``, ``container_port``, the environment the image needs, and how + to read and restore what it holds. Everything else is here. + """ + + engine: str = "" + image: str = "" + container_port: int = 0 + # Environment the image needs to come up with a known user, password and database. Values + # are formatted with ``user``, ``password`` and ``database``. + boot_env: dict[str, str] = {} + + def __init__( + self, + version: str | None = None, + image: str | None = None, + database: str = "alk", + user: str = "alk", + password: str | None = None, + ) -> None: + default = type(self).image + if image: + self.image = image + elif version: + self.image = f"{default.split(':')[0]}:{version}" + else: + self.image = default + self.database = database + self.user = user + self.password = password or secrets.token_hex(16) + self.container = f"alk-store-{secrets.token_hex(6)}" + self.network = os.environ.get(NETWORK, "").strip() + self.host = "127.0.0.1" + self.port: int | None = None + self._started = False + + # -- lifecycle ------------------------------------------------------------------- + + def start(self) -> None: + """Stand the container up and block until it answers. Idempotent.""" + if self._started: + return + environment: list[str] = [] + for name, template in self.boot_env.items(): + environment += [ + "--env", + f"{name}={template.format(user=self.user, password=self.password, database=self.database)}", + ] + docker( + "run", + "--detach", + "--name", + self.container, + "--label", + f"{LABEL}=1", + *environment, + *(("--network", self.network) if self.network else ()), + # Bound to loopback and given whatever port is free, so parallel runs on one + # machine never collide. Kept even on a shared network, where it is what lets + # someone on the host open a client against a running scenario. + "--publish", + f"127.0.0.1::{self.container_port}", + self.image, + ) + self._started = True + if self.network: + self.host, self.port = self.container, self.container_port + else: + self.port = self._published_port() + self._await_ready() + + def stop(self) -> None: + """Remove the container. Safe when it never started, so teardown needs no guard.""" + if not self._started: + return + docker("rm", "--force", "--volumes", self.container, check=False) + self._started = False + self.port = None + + def _published_port(self) -> int: + mapping = docker("port", self.container, f"{self.container_port}/tcp") + if not mapping: + raise StoreError( + f"{self.container} published no port for {self.container_port}/tcp" + ) + # "127.0.0.1:32768", or several lines when both stacks are bound. + return int(mapping.splitlines()[0].rsplit(":", 1)[1]) + + def _await_ready(self) -> None: + """Poll until the engine answers, and say what went wrong if it never does. + + A container that is running is not an engine that is ready: most database images start, + run their own initialisation, restart once, and only then listen. Connecting is the + only honest test, which is why this asks the subclass to really connect rather than + checking that the process exists. + """ + deadline = time.monotonic() + READY_TIMEOUT_SECONDS + last: Exception | None = None + while time.monotonic() < deadline: + try: + self.probe() + return + except Exception as exc: # noqa: BLE001 - any failure means not ready yet + last = exc + time.sleep(0.25) + logs = docker("logs", "--tail", "20", self.container, check=False) + raise StoreError( + f"{self.container} did not answer within {READY_TIMEOUT_SECONDS:.0f}s: {last}\n" + f"last lines of its log:\n{logs}" + ) + + def probe(self) -> None: + """Really talk to the engine. Anything raised means "not ready yet".""" + raise NotImplementedError + + # -- what the agent is pointed at ------------------------------------------------ + + def dsn(self) -> str: + """The connection string to hand the agent, in place of its own.""" + raise NotImplementedError + + def env(self, variable: str) -> dict[str, str]: + """The DSN under the name this agent reads it from. + + Redirecting an agent is usually one environment variable, and which one is a fact about + the agent rather than about us -- so it is named by the caller, never assumed here. + """ + return {variable: self.dsn()} + + def address(self) -> tuple[str, int]: + if not self._started or self.port is None: + raise StoreError("the store has not been started, so it has no address yet") + return self.host, self.port + + +def strays() -> list[str]: + """Containers the harness started that are still running. + + A killed run leaves its container behind, and the next one has no way to know it is not the + owner. Naming them is enough; removing them is the caller's decision. + """ + listed = docker( + "ps", "--filter", f"label={LABEL}=1", "--format", "{{.Names}}", check=False + ) + return [name for name in listed.splitlines() if name.strip()] diff --git a/harness/src/agent_harness/world/stores/inprocess.py b/harness/src/agent_harness/world/stores/inprocess.py new file mode 100644 index 0000000..489fb51 --- /dev/null +++ b/harness/src/agent_harness/world/stores/inprocess.py @@ -0,0 +1,340 @@ +"""The agent's own data, held where the agent holds it. + +Plenty of real agents keep their state in memory, loaded from files their repository ships, and +they are not unusual. There is no engine to stand up for those, no port and no connection string. +Standing up a database for them and hoping the agent notices would be exactly the replication +this path exists to avoid. + +So the store is the structure itself, and the agent's own loader is what fills it. The tools +under test then run against that structure the same way they run in production, because it *is* +the thing they run against: unmodified code, its real data, and a copy taken before each scenario +so the next one starts where the last one began. + +With no loader given, this holds nothing at all and says so. That is the honest description of a +world whose records the agent's code keeps on itself rather than in anything a store can reach, +and it exists so such a world is not described as a database it does not have. +""" + +from __future__ import annotations + +import copy +import json +from pathlib import Path +from typing import Any, Callable, Mapping, Sequence + +from . import Snapshot, StoreError + +# Carried alongside a record whose group is keyed rather than listed, because the key is usually +# the id a check needs to name and rebuilding the group without it would throw it away. +ID = "_id" + + +class InProcessStore: + """The agent's own in-memory data, as a store. + + ``loader`` is the agent's function, imported from the agent's repository and called, never + reimplemented, so what is held is what the agent would hold on a cold start. + """ + + engine = "in_process" + key = "in_process" + # Deliberately not state.json, which the snapshot uses for the agent's own state object. Two + # different things sharing one filename means whichever is written second wins, and the world + # comes back with its records on the wrong side of the seam: the store empty, everything in + # the agent's state, and the mutation gate then emptying a store that was never holding it. + FILE = "collections.json" + + def __init__( + self, + database: str | Path = "", + *, + loader: Callable[[], dict[str, Any]] | None = None, + module: str = "", + function: str = "load_data", + root: str | Path = "", + **_ignored: Any, + ) -> None: + # Takes the same arguments as any other store and uses most of them only when there is a + # loader, so opening one is the same call whichever kind it turns out to be. + self.database = str(database or "") + self.loader = loader + self.module = module + self.function = function + self.root = str(root or "") + self.data: dict[str, Any] = {} + self._started = False + + # -- lifecycle ------------------------------------------------------------------- + + def start(self) -> None: + """Load the agent's data by calling the agent's own loader, if there is one.""" + if self._started or (self.loader is None and not self.module): + return + if self.loader is None: + self.loader = self._imported() + loaded = self.loader() + if not isinstance(loaded, dict): + raise StoreError( + f"{self.function} returned {type(loaded).__name__}, not a dict of named groups, " + "so there is nothing a check could read by name" + ) + self.data = loaded + self._started = True + + def _imported(self) -> Callable[[], dict[str, Any]]: + """The agent's loader, imported from the agent's repository. + + Deliberately an import of their code rather than a reimplementation of it. If it will not + import, that is worth stopping for: the alternative is inventing data and grading the + agent against a world it has never seen. + """ + import importlib + import sys + + if self.root and self.root not in sys.path: + sys.path.insert(0, self.root) + try: + found = importlib.import_module(self.module) + except ImportError as exc: + raise StoreError( + f"cannot import {self.module!r} from {self.root or 'sys.path'}: {exc}. The " + "agent's own dependencies have to be importable for its loader to run." + ) from exc + loader = getattr(found, self.function, None) + if not callable(loader): + raise StoreError(f"{self.module}.{self.function} is not a function") + return loader + + def stop(self) -> None: + self.data = {} + self._started = False + + def dsn(self) -> str: + """Nothing connects to this, which is the point. + + Reported rather than raised: a store with no address is a fact about this kind of agent, + not a failure, and it is recorded so nothing later goes looking for a connection string + that was never going to exist. + """ + return "inprocess://" + + # -- statements ------------------------------------------------------------------ + + def apply(self, script: str) -> None: + """Run a snippet against the data, with ``data`` in scope and nothing else. + + How a seed is expressed for a store with no query language: the same Python the agent's + own code would use to reach into its structures. + """ + if not script.strip(): + return + namespace: dict[str, Any] = {"data": self.data, "json": json} + try: + exec(compile(script, "", "exec"), namespace) # nosec B102 + except Exception as exc: # noqa: BLE001 - the caller's snippet, reported as given + raise StoreError(f"{type(exc).__name__}: {exc}") from exc + + def execute(self, statement: str, params: Sequence[Any] = ()) -> int: + raise StoreError( + "this agent keeps its state in its own code, so there is no query language to run " + "statements in. Change the world through the agent's own tools, or through " + "world.put, world.change and world.drop." + ) + + def query(self, statement: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]: + return [] + + # -- records --------------------------------------------------------------------- + + def collections(self) -> list[str]: + return sorted(self.data) + + def holds(self, collection: str) -> bool: + return collection in self.data + + def records(self, collection: str) -> list[dict[str, Any]]: + return self._rows(self.data.get(collection)) + + def state(self) -> dict[str, list[dict[str, Any]]]: + """Every group and its records, in the shape the checks already expect. + + The agent's structures are usually keyed by id rather than listed, so a mapping becomes + records with the key carried along. Without that a check counting records in a group + would be counting nothing, and the id it needs to name would have been thrown away. + """ + return {name: self._rows(group) for name, group in self.data.items()} + + @staticmethod + def _rows(group: Any) -> list[dict[str, Any]]: + if isinstance(group, dict): + return [ + {ID: key, **value} if isinstance(value, dict) else {ID: key, "value": value} + for key, value in group.items() + ] + if isinstance(group, list): + return [row if isinstance(row, dict) else {"value": row} for row in group] + if group is None: + return [] + return [{"value": group}] + + def start_collection(self, collection: str, *, keyed: bool = False) -> None: + """Make a collection that does not exist yet. + + For an agent with no store of its own, every collection is one the harness invents, so + there is nothing to declare them in advance the way a schema does for a database. + """ + if collection not in self.data: + self.data[collection] = {} if keyed else [] + + def add(self, collection: str, record: Mapping[str, Any]) -> int: + group = self.data.get(collection) + if isinstance(group, list): + group.append(dict(record)) + return 1 + if isinstance(group, dict): + written = dict(record) + identifier = written.pop(ID, None) + if identifier is None: + raise KeyError( + f"{collection} is keyed, so a new record needs its key given as {ID!r}" + ) + group[identifier] = written + return 1 + raise KeyError(f"no group {collection!r} here to add to") + + def amend( + self, collection: str, key: str, changes: Mapping[str, Any], *, by: str = "" + ) -> int: + group = self.data.get(collection) + if isinstance(group, dict) and not by: + if key not in group: + return 0 + group[key].update(dict(changes)) + return 1 + for row in self._writable(collection, group, key, by): + row.update(dict(changes)) + return len(self._writable(collection, group, key, by)) + + def remove(self, collection: str, key: str = "", *, by: str = "") -> int: + group = self.data.get(collection) + if isinstance(group, dict): + if not key: + gone = len(group) + group.clear() + return gone + if by: + matched = [name for name, row in group.items() if _reads(row, by) == key] + else: + matched = [key] if key in group else [] + for name in matched: + group.pop(name, None) + return len(matched) + if isinstance(group, list): + if not key: + gone = len(group) + group.clear() + return gone + if not by: + raise KeyError( + f"{collection} is a list, so removing one record needs the field it is keyed on" + ) + kept = [row for row in group if _reads(row, by) != key] + gone = len(group) - len(kept) + group[:] = kept + return gone + raise KeyError(f"no group {collection!r} here to remove from") + + def _writable( + self, collection: str, group: Any, key: str, by: str + ) -> list[dict[str, Any]]: + if group is None: + raise KeyError(f"no group {collection!r} here to change") + if not by: + raise KeyError( + f"{collection} is a list, so changing a record needs the field it is keyed on" + ) + if isinstance(group, dict): + # A keyed group stores the key as the mapping's key, because `add` pops ``_id`` out + # of the record to put it there. So asking to match on ``_id`` finds nothing, changes + # nothing, and returns zero, which a scenario's setup does not look at: the run is + # then graded against a world that was never set up. The key is answered here as + # though it were still a field, which is what whoever wrote it meant. + if by == ID and key in group: + row = group[key] + return [row] if isinstance(row, dict) else [] + found: Any = group.values() + else: + found = group + return [row for row in found if isinstance(row, dict) and _reads(row, by) == key] + + # -- going back ------------------------------------------------------------------ + + def clear(self) -> None: + """Empty every group, keeping its shape: the agent's own code indexes into these.""" + for name, group in self.data.items(): + if isinstance(group, dict): + group.clear() + elif isinstance(group, list): + group.clear() + else: + self.data[name] = None + + def freeze(self) -> Snapshot: + """A deep copy. Nothing sits behind these records, so there are no counters to carry.""" + return Snapshot(rows=copy.deepcopy(self.state()), counters={}) + + def restore(self, snapshot: Snapshot) -> None: + """Put the structure back the way the agent's loader left it. + + Rebuilt from the records rather than kept as a second copy, so restore is checked against + exactly what ``state`` reports: the thing the gate compares and the thing a check reads + are then the same thing, and cannot drift apart. + """ + rebuilt: dict[str, Any] = {} + for name, rows in snapshot.rows.items(): + original = self.data.get(name) + if isinstance(original, list): + rebuilt[name] = [ + row["value"] if set(row) == {"value"} else dict(row) + for row in copy.deepcopy(rows) + ] + continue + keyed: dict[str, Any] = {} + for row in copy.deepcopy(rows): + identifier = row.pop(ID, None) + if identifier is None: + continue + keyed[identifier] = row.get("value") if set(row) == {"value"} else row + rebuilt[name] = keyed + # A group the snapshot does not mention is emptied, not carried over: restore has to be + # able to reproduce a snapshot that holds nothing, or the gate cannot empty the store to + # find out whether the checks actually bite. The key itself stays, with its original + # shape, because the agent's own code indexes into it and would not survive its absence. + for name, group in self.data.items(): + if name not in rebuilt: + rebuilt[name] = [] if isinstance(group, list) else {} + self.data.clear() + self.data.update(rebuilt) + + def save_to(self, path: str | Path) -> None: + if not self.data: + return + root = Path(path) + root.mkdir(parents=True, exist_ok=True) + (root / self.FILE).write_text( + json.dumps(self.data, indent=2, default=str), encoding="utf-8" + ) + + def load_from(self, path: str | Path) -> None: + held = Path(path) / self.FILE + if not held.exists(): + return + self.data = json.loads(held.read_text(encoding="utf-8")) + self._started = True + + def close(self) -> None: + return None + + +def _reads(row: Any, field: str) -> Any: + return row.get(field) if isinstance(row, dict) else None diff --git a/harness/src/agent_harness/world/stores/postgres.py b/harness/src/agent_harness/world/stores/postgres.py new file mode 100644 index 0000000..d8415fb --- /dev/null +++ b/harness/src/agent_harness/world/stores/postgres.py @@ -0,0 +1,221 @@ +"""Postgres, as the worked example of what an engine has to supply. + +This is not "the database the harness supports". It is the reference: when the build stage +finds an agent on ClickHouse or MySQL or DuckDB, what it writes is a class this shape, and +what it has to work out is only what is in this file below ``boot_env`` -- how to reach the +engine, how to read what it holds, and how to put that back. Starting a container, finding a +free port, waiting for the thing to genuinely answer and not leaking it afterwards are all in +``ContainerStore`` and are never rewritten. + +Nothing here knows what the agent's tools do. The agent keeps its own client, its own SQL and +its own migrations; the only thing that changed is the host on the far end of its DSN. The +schema is not invented either -- the build stage runs the agent's own migrations through +``apply``, so the tables are the agent's tables, spelled the way the agent spells them. A +schema we wrote ourselves would be a guess, and every check written against it would inherit +the guess. +""" + +from __future__ import annotations + +from typing import Any + +from . import Snapshot, StoreError +from .container import ContainerStore + + +def _psycopg() -> Any: + try: + import psycopg + except ImportError as exc: # pragma: no cover - depends on the install + raise StoreError( + "psycopg is not installed, so a Postgres store cannot be read. Install it with " + "`uv sync --extra harness-stores`." + ) from exc + return psycopg + + +class PostgresStore(ContainerStore): + """A Postgres container the agent under test is pointed at.""" + + engine = "postgres" + image = "postgres:16" + container_port = 5432 + boot_env = { + "POSTGRES_USER": "{user}", + "POSTGRES_PASSWORD": "{password}", + "POSTGRES_DB": "{database}", + } + + # -- how to reach it ------------------------------------------------------------- + + def dsn(self) -> str: + host, port = self.address() + return f"postgresql://{self.user}:{self.password}@{host}:{port}/{self.database}" + + def probe(self) -> None: + """Really connect. A running container is not yet a database that listens.""" + with _psycopg().connect(self.dsn(), connect_timeout=3) as connection: + connection.execute("SELECT 1") + + def _connect(self) -> Any: + """A short-lived autocommit connection. + + Deliberately not pooled and never held open. An idle transaction of ours would block + the ``TRUNCATE`` in ``restore``, and a reset that hangs on the harness's own connection + is a very expensive thing to debug. + """ + return _psycopg().connect(self.dsn(), autocommit=True) + + # -- how to read what it holds --------------------------------------------------- + + def apply(self, script: str) -> None: + """Run whatever was handed in: the agent's migrations, or its seed.""" + if not script.strip(): + return + with self._connect() as connection: + connection.execute(script) + + def _tables(self, connection: Any) -> list[str]: + rows = connection.execute( + "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename" + ).fetchall() + return [row[0] for row in rows] + + def _primary_key(self, connection: Any, table: str) -> list[str]: + """The primary key columns, used only to read rows back in a stable order.""" + rows = connection.execute( + """ + SELECT a.attname + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = %s::regclass AND i.indisprimary + ORDER BY array_position(i.indkey, a.attnum) + """, + (f'public."{table}"',), + ).fetchall() + return [row[0] for row in rows] + + def state(self) -> dict[str, list[dict[str, Any]]]: + """Every table and its rows, in the shape the checks already expect. + + Ordered by primary key where there is one. Without that the same data comes back in + whatever order the heap happens to hold it, and a check comparing the first row is + reading a coin toss rather than the agent's behaviour. + """ + with self._connect() as connection: + out: dict[str, list[dict[str, Any]]] = {} + for table in self._tables(connection): + key = self._primary_key(connection, table) + order = ( + " ORDER BY " + ", ".join(f'"{column}"' for column in key) if key else "" + ) + cursor = connection.execute(f'SELECT * FROM "{table}"{order}') + columns = [description[0] for description in cursor.description or []] + out[table] = [dict(zip(columns, row)) for row in cursor.fetchall()] + return out + + # -- how to put it back ---------------------------------------------------------- + + def freeze(self) -> Snapshot: + """Rows and sequence counters, which together are the whole mutable state.""" + with self._connect() as connection: + counters = { + row[0]: row[1] + for row in connection.execute( + "SELECT sequencename, last_value FROM pg_sequences " + "WHERE schemaname = 'public'" + ).fetchall() + if row[1] is not None + } + return Snapshot(rows=self.state(), counters=counters) + + def restore(self, snapshot: Snapshot) -> None: + """Put the data back exactly as the snapshot found it. + + Foreign keys are suspended for the duration rather than the rows being sorted into + dependency order: the snapshot was taken from a consistent database, so what goes back + is consistent by construction, and ordering it would be solving a problem we do not + have. Counters are set last, so the next scenario's first insert gets the id the first + scenario's did. + """ + with self._connect() as connection: + tables = self._tables(connection) + if not tables: + return + listed = ", ".join(f'"{table}"' for table in tables) + # One statement, so Postgres resolves the dependency order between them itself. + connection.execute(f"TRUNCATE TABLE {listed} RESTART IDENTITY CASCADE") + + connection.execute("SET session_replication_role = replica") + try: + for table, rows in snapshot.rows.items(): + if not rows or table not in tables: + continue + columns = list(rows[0]) + quoted = ", ".join(f'"{column}"' for column in columns) + placeholders = ", ".join(["%s"] * len(columns)) + statement = f'INSERT INTO "{table}" ({quoted}) VALUES ({placeholders})' + with connection.cursor() as cursor: + cursor.executemany( + statement, + [ + tuple(_adapt(row.get(column)) for column in columns) + for row in rows + ], + ) + finally: + connection.execute("SET session_replication_role = DEFAULT") + + for sequence, value in snapshot.counters.items(): + connection.execute( + "SELECT setval(%s, %s, true)", (f'public."{sequence}"', value) + ) + + # -- what a scenario changes ----------------------------------------------------- + + def add(self, collection: str, record: Any) -> int: + columns = list(record) + quoted = ", ".join(f'"{column}"' for column in columns) + placeholders = ", ".join(["%s"] * len(columns)) + with self._connect() as connection: + cursor = connection.execute( + f'INSERT INTO "{collection}" ({quoted}) VALUES ({placeholders})', + tuple(_adapt(record[column]) for column in columns), + ) + return cursor.rowcount + + def amend(self, collection: str, key: str, changes: Any, *, by: str = "") -> int: + if not by: + raise StoreError( + f"{collection} is a table, so changing a record needs the column it is keyed on" + ) + sets = ", ".join(f'"{column}" = %s' for column in changes) + with self._connect() as connection: + cursor = connection.execute( + f'UPDATE "{collection}" SET {sets} WHERE "{by}" = %s', + (*(_adapt(value) for value in changes.values()), key), + ) + return cursor.rowcount + + def remove(self, collection: str, key: str = "", *, by: str = "") -> int: + if key and not by: + raise StoreError( + f"{collection} is a table, so removing one record needs the column it is keyed on" + ) + statement = f'DELETE FROM "{collection}"' + (f' WHERE "{by}" = %s' if key else "") + with self._connect() as connection: + cursor = connection.execute(statement, (key,) if key else ()) + return cursor.rowcount + + +def _adapt(value: Any) -> Any: + """Hand back a value in the form psycopg will write. + + Only json needs saying: a ``jsonb`` column reads back as a dict or a list, and handing + either straight to an INSERT makes psycopg guess at a composite type instead. + """ + if isinstance(value, (dict, list)): + from psycopg.types.json import Jsonb + + return Jsonb(value) + return value diff --git a/harness/src/agent_harness/world/stores/prove.py b/harness/src/agent_harness/world/stores/prove.py new file mode 100644 index 0000000..6f262cb --- /dev/null +++ b/harness/src/agent_harness/world/stores/prove.py @@ -0,0 +1,173 @@ +"""Proving a store, without knowing which engine it is. + +The build stage writes the engine-specific half: which image, how to read what it holds, how +to put it back. That half is written per agent, by a model, against an engine nobody vetted in +advance -- so the only thing standing between a subtly wrong reset and a suite of results that +mean nothing is this file. + +Everything here is pure code and engine-independent. It never issues a query of its own, +because it cannot know the dialect; the one piece of engine-specific material it needs is a +``mutation`` -- any statement that changes something -- and even that is checked before it is +trusted, since a mutation that does nothing would make a broken restore look perfect. + +The sharp one is ``ids do not drift``. Rows going back is easy and most wrong restores manage +it; what they miss is the counter behind the rows, so the next scenario's first insert gets an +id continuing from the last one. Rather than ask what a counter is called on this engine -- +which is exactly the kind of thing we cannot know -- the same mutation is run twice from the +same starting point and the two results are compared. Any drift, in anything, shows up as a +difference. +""" + +from __future__ import annotations + +from typing import Any, Callable + +from ..probe import ProbeReport, ProbeResult +from . import Store + +STORE = "store" + +# A check over a proven store: a sentence when something is wrong, None when it held. +Check = Callable[[Store], "str | None"] + +# Whether the checks themselves can fail is asked elsewhere, by ``world/mutate.py``: it damages +# the whole world rather than only emptying the store, silences every tool as well, and runs each +# kind of damage against its own restored copy. Two gates asking the same question in different +# words is how one of them quietly stops being run, so there is deliberately only the one. + + +def _result(name: str, passed: bool, detail: str = "", kind: str = STORE) -> ProbeResult: + return ProbeResult(name=name, kind=kind, passed=passed, detail=detail) + + +def prove_store(store: Store, mutation: str) -> ProbeReport: + """Run a store through what it has to survive before any scenario is written against it. + + ``mutation`` is anything the engine accepts that changes what it holds -- one insert is + plenty. It comes from the build stage because it is the one part of this that has to be + written in the engine's own language. + + A failure here is ours, never the agent's. Nothing in this function involves the agent, so + a report with anything red means the environment is not yet a thing worth measuring against. + """ + report = ProbeReport() + + try: + baseline = store.freeze() + except Exception as exc: # noqa: BLE001 - a store that cannot be frozen fails here + report.results.append(_result("can be frozen", False, f"freeze raised: {exc}")) + return report + report.results.append(_result("can be frozen", True)) + + # Migrations that did not run leave a store with nothing in it, and every check written + # afterwards would pass or fail for reasons that have nothing to do with the agent. + if not baseline.rows: + report.results.append( + _result( + "holds a schema", + False, + "the store has no tables at all, so its migrations did not run", + ) + ) + return report + report.results.append( + _result("holds a schema", True, f"{len(baseline.rows)} tables") + ) + + seeded = sum(len(rows) for rows in baseline.rows.values()) + report.results.append( + _result( + "holds a seed", + seeded > 0, + f"{seeded} rows" if seeded else "every table is empty, so nothing can be presumed", + ) + ) + + # -- the mutation has to be worth something before it can prove anything ------------ + try: + store.apply(mutation) + except Exception as exc: # noqa: BLE001 - the caller's statement, reported as given + report.results.append( + _result("the mutation runs", False, f"{exc}") + ) + return report + report.results.append(_result("the mutation runs", True)) + + mutated = store.state() + if mutated == baseline.rows: + report.results.append( + _result( + "the mutation moves it", + False, + "the store is unchanged after it, so it cannot prove a restore works", + ) + ) + return report + report.results.append(_result("the mutation moves it", True)) + + # -- putting it back has to be exact ------------------------------------------------- + try: + store.restore(baseline) + except Exception as exc: # noqa: BLE001 + report.results.append(_result("restore runs", False, f"restore raised: {exc}")) + return report + report.results.append(_result("restore runs", True)) + + back = store.state() + report.results.append( + _result( + "restore is exact", + back == baseline.rows, + "" if back == baseline.rows else _difference(baseline.rows, back), + ) + ) + + # -- and it has to put back what is behind the rows, not only the rows --------------- + try: + store.apply(mutation) + again = store.state() + except Exception as exc: # noqa: BLE001 + report.results.append(_result("ids do not drift", False, f"{exc}")) + return report + + report.results.append( + _result( + "ids do not drift", + again == mutated, + "" + if again == mutated + else ( + "the same change from the same starting point produced something different " + "the second time, so the restore left a counter where it was: " + + _difference(mutated, again) + ), + ) + ) + + store.restore(baseline) + report.results.append( + _result("restore repeats", store.state() == baseline.rows) + ) + return report + + +def _difference(expected: dict[str, Any], found: dict[str, Any]) -> str: + """The first place two states disagree, said plainly. + + Whole-state diffs are unreadable at any real size, and the first disagreement is almost + always the whole story. + """ + for table in sorted(set(expected) | set(found)): + before, after = expected.get(table), found.get(table) + if before == after: + continue + if before is None: + return f"{table} appeared" + if after is None: + return f"{table} disappeared" + if len(before) != len(after): + return f"{table}: {len(before)} rows expected, {len(after)} found" + for index, (one, two) in enumerate(zip(before, after)): + if one != two: + return f"{table} row {index}: expected {one}, found {two}" + return "no difference found, which should not happen" diff --git a/harness/src/agent_harness/world/stores/sqlite.py b/harness/src/agent_harness/world/stores/sqlite.py new file mode 100644 index 0000000..e84eacd --- /dev/null +++ b/harness/src/agent_harness/world/stores/sqlite.py @@ -0,0 +1,228 @@ +"""Records in a SQLite file, or in memory. + +The default, because it needs nothing installed and nothing standing up. Saving to disk is a +copy of the database rather than a dump, so loading it back is exact and fast, which matters +because every scenario and every probe starts from that copy. +""" + +from __future__ import annotations + +import shutil +import sqlite3 +from pathlib import Path +from typing import Any, Mapping, Sequence + +from . import Records, Snapshot, StoreError + +# What SQLite hands out that is not itself a record. Only present once a table is declared +# AUTOINCREMENT, which is why its absence is normal rather than a gap. +COUNTERS = "sqlite_sequence" + + +class SqliteStore(Records): + engine = "sqlite" + key = "sqlite" + FILE = "world.sqlite" + + def __init__(self, database: str | Path = ":memory:", **_ignored: Any) -> None: + self.database = str(database) + self.connection = sqlite3.connect(self.database, check_same_thread=False) + self.connection.execute("PRAGMA foreign_keys = ON") + + # -- lifecycle ------------------------------------------------------------------- + + def start(self) -> None: + """Already up. Connecting is what ``__init__`` did, and there is no server to wait for.""" + + def stop(self) -> None: + self.close() + + def dsn(self) -> str: + return f"sqlite:///{self.database}" + + # -- statements ------------------------------------------------------------------ + + def execute(self, statement: str, params: Sequence[Any] = ()) -> int: + cursor = self.connection.execute(statement, tuple(params)) + self.connection.commit() + return cursor.rowcount + + def apply(self, script: str) -> None: + """Several statements at once, which is how a schema or a seed arrives.""" + if not script.strip(): + return + self.connection.executescript(script) + self.connection.commit() + + # The name this had before a store was asked to speak more than SQL. + script = apply + + def query(self, statement: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]: + cursor = self.connection.execute(statement, tuple(params)) + columns = [column[0] for column in (cursor.description or [])] + return [dict(zip(columns, row)) for row in cursor.fetchall()] + + # -- records --------------------------------------------------------------------- + + def collections(self) -> list[str]: + found = self.query( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" + ) + return [row["name"] for row in found] + + def holds(self, collection: str) -> bool: + return bool( + self.query( + "SELECT name FROM sqlite_master WHERE type='table' AND name = ?", [collection] + ) + ) + + def records(self, collection: str) -> list[dict[str, Any]]: + return self.query(f'SELECT * FROM "{collection}"') + + def add(self, collection: str, record: Mapping[str, Any]) -> int: + columns = ", ".join(f'"{name}"' for name in record) + marks = ", ".join("?" for _ in record) + return self.execute( + f'INSERT INTO "{collection}" ({columns}) VALUES ({marks})', list(record.values()) + ) + + def amend( + self, collection: str, key: str, changes: Mapping[str, Any], *, by: str = "" + ) -> int: + if not by: + raise KeyError( + f"{collection} is a table, so changing a record needs the column it is keyed on" + ) + sets = ", ".join(f'"{name}" = ?' for name in changes) + return self.execute( + f'UPDATE "{collection}" SET {sets} WHERE "{by}" = ?', [*changes.values(), key] + ) + + def remove(self, collection: str, key: str = "", *, by: str = "") -> int: + if key and not by: + raise KeyError( + f"{collection} is a table, so removing one record needs the column it is keyed on" + ) + sql = f'DELETE FROM "{collection}"' + (f' WHERE "{by}" = ?' if key else "") + return self.execute(sql, [key] if key else []) + + # -- going back, in memory ------------------------------------------------------- + + def freeze(self) -> Snapshot: + return Snapshot(rows=self.state(), counters=self._counters()) + + def restore(self, snapshot: Snapshot) -> None: + """Put the rows back, and the counters behind them. + + Foreign keys are dropped for the duration rather than the tables being sorted into + dependency order: any order is wrong for some schema, and a restore that fails on a + schema the agent really has is worse than one that trusts the snapshot it took itself. + """ + self.connection.execute("PRAGMA foreign_keys = OFF") + try: + for name in self.collections(): + self.connection.execute(f'DELETE FROM "{name}"') + for name, rows in snapshot.rows.items(): + for row in rows: + if not row: + continue + columns = ", ".join(f'"{column}"' for column in row) + marks = ", ".join("?" for _ in row) + self.connection.execute( + f'INSERT INTO "{name}" ({columns}) VALUES ({marks})', list(row.values()) + ) + self._reinstate(snapshot.counters) + self.connection.commit() + finally: + self.connection.execute("PRAGMA foreign_keys = ON") + + def _counters(self) -> dict[str, int]: + if not self.holds(COUNTERS): + return {} + return {row["name"]: row["seq"] for row in self.query(f"SELECT name, seq FROM {COUNTERS}")} + + def _reinstate(self, counters: Mapping[str, int]) -> None: + if not self.holds(COUNTERS): + return + self.connection.execute(f"DELETE FROM {COUNTERS}") + for name, seq in counters.items(): + self.connection.execute( + f"INSERT INTO {COUNTERS} (name, seq) VALUES (?, ?)", (name, seq) + ) + + def clear(self) -> None: + """Empty every table, whatever references what. + + Foreign keys are suspended for the duration rather than the tables being sorted into + dependency order. Deleting them one at a time in the wrong order fails on the referenced + ones, and a caller that swallows those failures is left believing it emptied a store that + still holds most of its data. + """ + self.connection.execute("PRAGMA foreign_keys = OFF") + try: + for name in self.collections(): + self.connection.execute(f'DELETE FROM "{name}"') + self.connection.commit() + finally: + self.connection.execute("PRAGMA foreign_keys = ON") + + def take(self, held: str | Path) -> None: + """Become a copy of another SQLite database: the agent's own. + + The whole file, schema and data together, rather than rows read out and written back. An + agent's real store carries things a reconstruction loses: its exact types, its indexes, + its keys, and every oddity in the data that its queries were actually written against. + """ + origin = sqlite3.connect(f"file:{Path(held)}?mode=ro", uri=True) + try: + with self.connection: + origin.backup(self.connection) + finally: + origin.close() + + # -- going back, on disk --------------------------------------------------------- + + def save_to(self, path: str | Path) -> None: + root = Path(path) + root.mkdir(parents=True, exist_ok=True) + # Settled first: an open read transaction makes the copy fail, and a handler that only + # read leaves one behind. + self.connection.commit() + held = root / self.FILE + # A world whose live database already is the saved file has nothing to copy, and copying + # it would be a backup onto its own file. SQLite retries a locked destination rather than + # refusing, so that does not fail: it hangs, with no error and no timeout, and the build + # stops dead somewhere nobody is looking. + if self._same_file(held): + return + copy = sqlite3.connect(held) + with copy: + self.connection.backup(copy) + copy.close() + + def _same_file(self, held: Path) -> bool: + if self.database == ":memory:": + return False + live = Path(self.database) + if not live.exists() or not held.exists(): + return str(live) == str(held) + return live.samefile(held) + + def load_from(self, path: str | Path) -> None: + held = Path(path) / self.FILE + if not held.exists(): + raise StoreError(f"no saved store at {held}") + if self.database == ":memory:": + origin = sqlite3.connect(held) + with self.connection: + origin.backup(self.connection) + origin.close() + return + self.connection.close() + shutil.copyfile(held, self.database) + self.connection = sqlite3.connect(self.database, check_same_thread=False) + self.connection.execute("PRAGMA foreign_keys = ON") + + def close(self) -> None: + self.connection.close() diff --git a/harness/src/agent_harness/world/stores/written.py b/harness/src/agent_harness/world/stores/written.py new file mode 100644 index 0000000..e82c111 --- /dev/null +++ b/harness/src/agent_harness/world/stores/written.py @@ -0,0 +1,180 @@ +"""A store the build stage wrote, for an engine nobody taught the harness in advance. + +This is what keeps "whatever the agent uses" from meaning "whatever we got around to +shipping". When the build stage finds an agent on an engine with no store in the tree, it +writes one: the image to run, the port it listens on, how to build a connection string for it, +and five functions saying how to talk to it. + +None of that is trusted. A reset written by a model for an engine nobody reviewed is exactly +the thing that fails silently -- rows go back, a counter does not, and every scenario after the +first is measured against a world that drifted. So a written store is registered, not accepted: +whether it works is decided by ``prove_store``, which runs the same change twice from the same +starting point and compares. Nothing here has to be right for that gate to be meaningful, which +is the only reason writing it at build time is safe at all. +""" + +from __future__ import annotations + +from typing import Any, Callable + +from . import Snapshot, StoreError, register_store +from .container import ContainerStore + +# The functions a written store defines. Fewer would not be enough for an arbitrary engine, and +# more would be us guessing at what engines have in common. The last three are what a scenario's +# own setup lands on: without them the environment can be stood up and read, but nothing can +# change a little of it, so every scenario would run against the same base. +REQUIRED = ("connect", "apply", "state", "freeze", "restore", "add", "amend", "remove") + +API = ( + "Your code defines exactly these functions:\n" + " def connect(dsn) -> a live client, already connected\n" + " def apply(db, script) -> run statements: migrations, or a seed\n" + " def state(db) -> {group: [row, ...]} for everything it holds\n" + " def freeze(db) -> (rows, counters)\n" + " def restore(db, rows, counters) -> put both back exactly\n" + " def add(db, group, record) -> insert one record, return how many landed\n" + " def amend(db, group, key, changes, by)-> update records where `by` equals `key`\n" + " def remove(db, group, key, by) -> delete those records; no key means all of them\n" + "Import whatever driver this engine needs at the top of the file; if it is not installed " + "you will be told which one is missing. `counters` is anything that keeps counting after " + "the rows are gone -- a sequence, an auto-increment. Restoring rows without it gives the " + "next scenario ids continuing from the last one. Engines that hand out nothing of the sort " + "return {}. Read state in a stable order, or a check comparing the first row is reading a " + "coin toss. `add`, `amend` and `remove` are what a scenario's setup calls, so they are the " + "difference between a suite of scenarios and one base world tested many times." +) + + +def _compile(code: str, engine: str) -> dict[str, Callable[..., Any]]: + """Turn the written code into its five functions, or say precisely what is missing.""" + namespace: dict[str, Any] = {} + try: + exec(compile(code, f"", "exec"), namespace) # nosec B102 + except ImportError as exc: + raise StoreError( + f"{engine} ops import something that is not installed: {exc}. Install the driver " + f"this engine needs, or use an engine whose driver is already present." + ) from exc + except SyntaxError as exc: + raise StoreError(f"{engine} ops do not parse: {exc}") from exc + + missing = [name for name in REQUIRED if not callable(namespace.get(name))] + if missing: + raise StoreError( + f"{engine} ops define {', '.join(sorted(n for n in REQUIRED if n not in missing)) or 'nothing'}" + f" but not {', '.join(missing)}.\n{API}" + ) + return {name: namespace[name] for name in REQUIRED} + + +def register_written( + *, + engine: str, + image: str, + container_port: int, + code: str, + boot_env: dict[str, str] | None = None, + dsn_template: str = "", +) -> type[ContainerStore]: + """Teach the harness an engine from code written at build time. + + Registered rather than accepted: this makes the engine available to ``declare_engine``, and + says nothing at all about whether its reset is correct. That is ``prove_store``'s to decide. + """ + if not engine.strip(): + raise StoreError("an engine needs a name") + if not image.strip(): + raise StoreError(f"{engine} needs an image to run") + if not container_port: + raise StoreError(f"{engine} needs the port it listens on") + ops = _compile(code, engine) + + class WrittenStore(ContainerStore): + pass + + WrittenStore.engine = engine + WrittenStore.image = image + WrittenStore.container_port = container_port + WrittenStore.boot_env = dict(boot_env or {}) + WrittenStore._ops = ops # type: ignore[attr-defined] + WrittenStore._dsn_template = ( + dsn_template or "{engine}://{user}:{password}@{host}:{port}/{database}" + ) + + def dsn(self: ContainerStore) -> str: + host, port = self.address() + return type(self)._dsn_template.format( # type: ignore[attr-defined] + engine=type(self).engine, + user=self.user, + password=self.password, + host=host, + port=port, + database=self.database, + ) + + def _client(self: ContainerStore) -> Any: + """A fresh client per operation. + + Never held open, for the same reason the Postgres store does not hold one: an idle + transaction of ours blocks the reset, and a reset that hangs on the harness's own + connection is a very expensive thing to debug. + """ + return type(self)._ops["connect"](self.dsn()) # type: ignore[attr-defined] + + def _with(self: ContainerStore, name: str, *args: Any) -> Any: + db = self._client() # type: ignore[attr-defined] + try: + return type(self)._ops[name](db, *args) # type: ignore[attr-defined] + finally: + closer = getattr(db, "close", None) + if callable(closer): + closer() + + def probe(self: ContainerStore) -> None: + db = self._client() # type: ignore[attr-defined] + closer = getattr(db, "close", None) + if callable(closer): + closer() + + def apply(self: ContainerStore, script: str) -> None: + if not script.strip(): + return + self._with("apply", script) # type: ignore[attr-defined] + + def state(self: ContainerStore) -> dict[str, list[dict[str, Any]]]: + return self._with("state") # type: ignore[attr-defined] + + def freeze(self: ContainerStore) -> Snapshot: + rows, counters = self._with("freeze") # type: ignore[attr-defined] + return Snapshot(rows=rows, counters=counters or {}) + + def restore(self: ContainerStore, snapshot: Snapshot) -> None: + self._with("restore", snapshot.rows, snapshot.counters) # type: ignore[attr-defined] + + def add(self: ContainerStore, collection: str, record: Any) -> int: + return int(self._with("add", collection, dict(record)) or 0) # type: ignore[attr-defined] + + def amend( + self: ContainerStore, collection: str, key: str, changes: Any, *, by: str = "" + ) -> int: + return int(self._with("amend", collection, key, dict(changes), by) or 0) # type: ignore[attr-defined] + + def remove(self: ContainerStore, collection: str, key: str = "", *, by: str = "") -> int: + return int(self._with("remove", collection, key, by) or 0) # type: ignore[attr-defined] + + WrittenStore.add = add # type: ignore[assignment] + WrittenStore.amend = amend # type: ignore[assignment] + WrittenStore.remove = remove # type: ignore[assignment] + WrittenStore.dsn = dsn # type: ignore[assignment] + WrittenStore._client = _client # type: ignore[attr-defined] + WrittenStore._with = _with # type: ignore[attr-defined] + WrittenStore.probe = probe # type: ignore[assignment] + WrittenStore.apply = apply # type: ignore[assignment] + WrittenStore.state = state # type: ignore[assignment] + WrittenStore.freeze = freeze # type: ignore[assignment] + WrittenStore.restore = restore # type: ignore[assignment] + WrittenStore.__name__ = f"{engine.title().replace('_', '')}Store" + + register_store(engine, WrittenStore) + return WrittenStore diff --git a/harness/src/agent_harness/world/tools.py b/harness/src/agent_harness/world/tools.py new file mode 100644 index 0000000..7a52638 --- /dev/null +++ b/harness/src/agent_harness/world/tools.py @@ -0,0 +1,1185 @@ +"""The tools that build a world, and the gate that decides it may be saved. + +A deliberately narrow surface. The builder gets no generic file write, because a guardrail needs +something to sit behind: every action goes through a tool that can execute it, check it, and say +what went wrong. Interface design work on coding agents is consistent that this beats handing +over raw access and hoping. + +Three habits throughout, for the same reason: + +- **execute immediately.** A handler is run the moment it is defined, so a mistake comes back on + the next turn rather than at save time. +- **say what happened, briefly.** Counts and names, never dumps. More context measurably makes + agents worse at this. +- **never answer with nothing.** "0 rows inserted" is a result; an empty string is a puzzle. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from claude_agent_sdk import create_sdk_mcp_server, tool + +from ..catalogue import SubGoal, load_catalogue, save_catalogue, validate_sub_goal +from ..simulator import load_simulator_prompt, save_simulator_prompt, validate_simulator_prompt +from ..tools import brief as _brief, schema +from ..amend import add_rule, drop_rule, fix_tool, set_modality, unreachable, widen +from ..contract import AgentContract +from .kinds import for_contract +from ..checks import run_check, run_world_check +from .mutate import UNDAMAGED, blind, unnoticed +from .probe import dirty_state, probe +from .runtime import GeneratedWorld +from .snapshot import MANIFEST, read_manifest, restore, save +from .stores.written import API as OPS_API + +WORLD_SERVER = "world" + +# What a handler is actually given. Said again here, and not only in the skill, because this is +# where the mistake surfaces: a handler that crashed has a model reading *this* message, and an +# error naming the failure without naming the API produces the same wrong guess again. Three +# identical attempts at one handler is what that costs. +DB_API = ( + "Inside a handler, `db` reads the world two ways and has no cursors.\n\n" + "Works on every world, database or not:\n" + ' db.records("orders") -> every record in a collection, as dicts\n' + ' db.find("orders", status="new") -> the ones whose fields all match\n' + ' db.collections() -> the collection names\n' + ' db.add("orders", {"id": "o1"}) -> put one record in\n\n' + "Only where this world has a query language, which not every agent does:\n" + ' db.query("SELECT * FROM t WHERE id = ?", [x]) -> list of dicts, [] if none\n' + ' db.one("SELECT * FROM t WHERE id = ?", [x]) -> one dict, or None\n' + ' db.execute("INSERT INTO t (a) VALUES (?)", [x]) -> number of rows changed\n\n' + "If this world has no connection, those three raise and the first four are what to use. " + "Records are dicts read by field name. db.execute returns a count, not a cursor, so calling " + ".fetchone(), .fetchall() or .lastrowid on any of these is a mistake. You also have `args`, " + "`ToolError` and `json`, and nothing else. Do not import anything." +) + +# Below this, the world is not good enough to build tests on. Synthesis work that measures this +# converges on roughly this bar, and rejects a quarter to a third of what it generates. +ACCEPTABLE = 0.85 + +# What a world check is, said where the mistake surfaces. A check that inspects nothing is +# the failure this whole mechanism exists to catch, so the answer says what "inspects +# something" means rather than only that the check was rejected. +WORLD_CHECK_HELP = ( + "A world check is Python defining check(world), returning None when it holds or a " + "sentence saying what is wrong.\n\n" + "`world.state()` gives every collection this world has. **A collection is not always a list.**\n" + "A table gives a list of records. A collection the agent's own code keeps is often a mapping " + "keyed by identifier, and iterating that yields the keys, which are strings. Reading a field " + "off one of those is where a check written for the wrong shape fails.\n" + " held = world.state()['some_collection']\n" + " records = list(held.values()) if isinstance(held, dict) else held\n" + " wanted = [one for one in records if one.get('status') == 'pending']\n" + "The shapes this world actually has are listed below, so write for those rather than " + "guessing.\n\n" + "A check also has to inspect something that could be wrong. One that returns None without " + "reading the world passes forever, and it is rejected once the world is broken on purpose " + "and it stays green." +) + + +def _shapes(world: Any) -> str: + """What this world's collections are. Asked of the world, so every gate says the same thing.""" + return world.shapes() + +# What to read when a binding to the agent's own code will not run. The failure is nearly always +# the shape of the call rather than the code being unreachable, so the answer says what the +# shapes are instead of only reporting the exception. +BINDING_SCOPE = ( + "Inside a binding, and inside a factory expression, these are the only names that exist:\n" + " args the arguments the agent passed, as a dict\n" + " db the world. db.state is the agent's own state, as adopt_state loaded it\n" + " ToolError to refuse\n" + " json\n" + "plus whatever the binding itself imports from the agent's source. There is no `userdata`, no " + "`state`, no framework context and no session: if the callable needs one of those, it has to " + "be constructed in the factory expression out of what is listed above, or it cannot be " + "reached from here at all." +) + +ADOPT_HELP = ( + "A binding is how one of the agent's own callables is reached. Four things can be wrong:\n" + " - the module path. It is imported from the agent's source root, so use the path its own " + "code would use, e.g. package.module.file, not a filesystem path\n" + " - the style. 'function' for a module-level def, 'staticmethod' for one on a class, " + "'method' when an instance has to exist first, in which case `factory` is the expression " + "that builds it\n" + " - first_arg. If the callable takes the agent's state as its first argument, name it here " + "and the world passes what adopt_state loaded. Leave it empty when the callable connects " + "for itself\n" + " - smoke_arguments. These are passed as keywords, so they have to match the callable's own " + "parameter names, and their values should be real: look at the world first and use an " + "identifier that exists, or the call only ever proves the tool can say no\n" + "If the tool genuinely cannot be reached without editing the agent, say so and ask. Do not " + "write a replacement for it." +) + + +def _size(value: Any) -> Any: + return len(value) if isinstance(value, (list, dict, tuple, str)) else value + + +# What a store file is called, when nobody has said where it is. Extensions rather than names, +# because the name is the agent's business and the extension is the convention. +STORE_SUFFIXES = (".db", ".sqlite", ".sqlite3", ".duckdb", ".dump", ".sql") + + +def _stores_here(source_root: str) -> str: + """Where the agent's code is, and which files under it look like a store. + + Said rather than left to be guessed. A message that reports a path was wrong without saying + what the right ones are turns one call into a search, and the search is over a filesystem this + stage deliberately cannot list. + """ + if not source_root: + return ( + "This stage was not told where the agent's code lives, so a relative path has nothing " + "to resolve against. Give an absolute path, or say that the source root is missing." + ) + root = Path(source_root) + seen: list[str] = [] + for path in sorted(root.rglob("*")): + if len(seen) >= 12: + break + if path.is_file() and path.suffix.lower() in STORE_SUFFIXES: + size = path.stat().st_size + measure = f"{size // 1024} KB" if size else "empty" + seen.append(f" {path.relative_to(root)} ({measure})") + if not seen: + return ( + f"The agent's code is at {root}, and nothing under it looks like a store. If it " + "builds or downloads one on first run, say so and ask rather than inventing data." + ) + return "The agent's code is at " + str(root) + ", and these look like stores:\n" + "\n".join(seen) + + +def _binding(*, module: str, called: str, style: str, first_arg: str, factory: str) -> str: + """The handler that calls one of the agent's own callables. + + Written as source rather than held as a closure, so it is saved with the world, readable by + whoever wants to know what actually ran, and restored exactly as every other handler is. + + ``called`` may be a dotted path inside the module, which is how a staticmethod is reached: + ``CancelPendingOrder.invoke`` imports the class and calls the method on it. Only the first + segment is imported. + """ + root = called.split(".")[0] + reach = f"from {module} import {root}" if module else "" + state = "db.state, " if first_arg else "" + # Their code may be async, which is true of every framework-decorated tool. The result is + # settled here rather than by the caller so that a handler stays synchronous, which is what + # every other part of the world already assumes. + if style == "method": + built = factory or f"{root}()" + attr = called.split(".", 1)[1] if "." in called else "__call__" + return ( + f"{reach}\n" + "from agent_harness.world.runtime import settled\n\n" + "def handle(args, db):\n" + f" instance = {built}\n" + f" return settled(instance.{attr}({state}**args))\n" + ) + return ( + f"{reach}\n" + "from agent_harness.world.runtime import settled\n\n" + "def handle(args, db):\n" + f" return settled({called}({state}**args))\n" + ) + + +def _ok(text: str) -> dict[str, Any]: + return {"content": [{"type": "text", "text": text}]} + + +def _err(text: str) -> dict[str, Any]: + return {"content": [{"type": "text", "text": text}], "is_error": True} + + +def world_tools( + contract: AgentContract, destination: Path, *, source_root: str = "" +) -> Any: + """A server exposing the world-building surface for one agent. + + ``source_root`` is where the agent's own code lives. With it, a tool can be bound to the + agent's own implementation; without it the agent was given as a specification and its + tools have to be written here. + """ + # An existing world is picked up rather than replaced. Amending one is the ordinary case + # once it has been built once, and starting empty every time would mean rebuilding a + # catalogue from scratch to add a single item to it. + existing = (destination / MANIFEST).exists() + # The store comes from what the contract found, not from a default. An agent whose tools keep + # their own state has no database, and opening one for it would be carrying something unused + # and describing the world as something it is not. + named = str(getattr(getattr(contract, "data_store", None), "kind", "") or "") + world = restore(destination) if existing else GeneratedWorld(":memory:", kind=named) + world.name = contract.agent + world.refusal_signature = contract.refusal_signature + if source_root: + world.reach(source_root) + kind = for_contract(contract) + catalogue = load_catalogue(destination) + scores: list[float] = [] + # The checks that decide whether this world is usable, written here rather than fixed in + # advance, because what makes a world usable is a judgement about this agent. + world_checks: dict[str, str] = dict(read_manifest(destination).get("world_checks") or {}) if existing else {} + # How many times each tool has been attempted, so a binding that cannot be made to work + # is told to stop rather than tried indefinitely. + tried: dict[str, int] = {} + sequences: list[dict[str, Any]] = ( + list(read_manifest(destination).get("sequences") or []) if existing else [] + ) + + def _verified() -> tuple[list[str], list[str], dict[str, list[str]]]: + """How the world's own checks fare, and which of them cannot fail. + + Run against the world as it stands, and then against worlds broken on purpose. A check + that stays green through every kind of damage is reported as blind: it is not verifying + anything, whatever it claims to inspect. + """ + import tempfile + + failing = [ + name + for name, source in sorted(world_checks.items()) + if not run_world_check(source, world, name=name).held + ] + if not world_checks: + return failing, [], {} + # Snapshotted first so each mutation gets its own copy and none inherits another's + # damage. The world being built is never touched. + held = Path(tempfile.mkdtemp()) + save(world, held, notes="mutation", sequences=sequences) + survived = unnoticed( + held, + sorted(world_checks.items()), + run=lambda source, broken: run_world_check(source, broken, name="check"), + restore=restore, + ) + return failing, blind(survived), survived + + @tool( + "create_schema", + "Run CREATE TABLE statements. Call once with the whole schema; call again to alter it.", + {"sql": str}, + ) + async def create_schema(args: dict[str, Any]) -> dict[str, Any]: + try: + world.connection.executescript(args["sql"]) + world.connection.commit() + except Exception as failed: + return _err(f"schema rejected: {failed}") + tables = sorted(world.state()) + return _ok(f"{len(tables)} tables: {', '.join(tables) or 'none'}") + + @tool( + "seed", + "Put records into a collection. Rows is a list of objects whose keys are field names. " + "Works whether or not this world has a database: a collection that does not exist yet is " + "made, which is how an agent with no store of its own gets one.", + {"table": str, "rows": list}, + ) + async def seed(args: dict[str, Any]) -> dict[str, Any]: + table, rows = str(args["table"]), args.get("rows") or [] + written = 0 + for row in rows: + if not isinstance(row, dict) or not row: + continue + try: + # Through the world rather than the connection, so this is the same call for a + # table, for a structure the agent's own code keeps, and for an agent that has no + # store at all and whose collections the harness is inventing. + world.put(table, row) + written += 1 + except Exception as failed: + return _err( + f"{written} records written, then {table} rejected one: {failed}\n" + f"{_shapes(world)}" + ) + total = len(world.state().get(table, [])) + return _ok(f"{written} records put into {table}; {total} there now") + + @tool( + "change_data", + "Change or remove rows already in the world: one UPDATE or DELETE statement. Seeding " + "only ever inserts, so without this a row put in wrong can never be taken out, and the " + "only way left to make a check pass is to change the contract, which is the wrong " + "repair. Use inspect_world to read; this is for changing.", + {"sql": str}, + ) + async def change_data(args: dict[str, Any]) -> dict[str, Any]: + statement = str(args.get("sql") or "").strip() + verb = statement.split(None, 1)[0].upper() if statement else "" + if verb not in ("UPDATE", "DELETE"): + return _err( + "this runs one UPDATE or DELETE. Use seed to add rows, create_schema to change " + "the shape of a table, and inspect_world to look." + ) + try: + changed = world.connection.execute(statement).rowcount + world.connection.commit() + except Exception as failed: + world.connection.rollback() + return _err(f"rejected: {failed}") + counts = ", ".join(f"{n}: {len(r)}" for n, r in sorted(world.state().items())) + return _ok(f"{changed} rows changed. The world now holds {counts}") + + @tool( + "define_handler", + "Define one tool's implementation. The source must define handle(args, db) and is run " + "immediately against the seeded world, so errors come straight back.", + schema( + {"tool_name": str, "source": str, "smoke_arguments": dict}, + ["tool_name", "source"], + ), + ) + async def define_handler(args: dict[str, Any]) -> dict[str, Any]: + name = str(args["tool_name"]) + if name not in contract.tool_names(): + return _err( + f"{name!r} is not a tool this agent has. It has: " + f"{', '.join(sorted(contract.tool_names()))}" + ) + # Writing a replacement for a tool the agent already implements is the one thing this + # stage must not do. It changes what is being tested from the agent's behaviour to our + # reading of it, and the difference does not show up anywhere afterwards. + if contract.adoptable(name): + entry = contract.entry_for(name) + return _err( + f"{name} already has an implementation, so it is not ours to write. Use " + f"adopt_tool to bind to {entry.module}.{entry.callable} instead.\n" + "If you have tried and it genuinely cannot be reached from here, say so with " + "cannot_reach_tool and what stopped it. That records the reason on the contract " + "and then lets you write one. Do not write a replacement without it: a generated " + "stand-in nobody knows is a stand-in is worse than a tool we admit we could not " + "run." + ) + world.handlers[name] = str(args["source"]) + # Same reason as adopting one: running it to prove it works must leave the world as it was. + held = world.checkpoint() + call = world.call(name, args.get("smoke_arguments") or {}) + world.revert(held) + if call.refused: + return _ok( + f"{name} defined. Smoke call refused, which is a working refusal: {call.error}" + ) + if not call.ok: + del world.handlers[name] + said = f"{name} not kept, it crashed on its smoke call: {call.error}" + # A crash is nearly always the handler reaching for something it does not have, so + # the answer says what it does have rather than only what went wrong. + return _err(f"{said}\n\n{DB_API}") + return _ok(f"{name} defined and ran. Returned {_brief(call.result)}") + + @tool( + "adopt_state", + "Load the agent's own starting state by calling its own loader, so the world holds what " + "the agent really has rather than a copy of it. Give the module and the callable, for " + "example the function that reads its data files.", + schema({"module": str, "callable": str}, ["module", "callable"]), + ) + async def adopt_state(args: dict[str, Any]) -> dict[str, Any]: + module = str(args["module"]) + called = str(args["callable"]) + world.reach(source_root) + try: + loaded = __import__(module, fromlist=[called]) + factory = getattr(loaded, called) + world.state_object = factory() + except Exception as raised: + return _err( + f"could not load state with {module}.{called}: " + f"{type(raised).__name__}: {raised}\n{ADOPT_HELP}" + ) + summary = ( + {key: _size(value) for key, value in world.state_object.items()} + if isinstance(world.state_object, dict) + else type(world.state_object).__name__ + ) + return _ok(f"state loaded from {module}.{called}: {json.dumps(summary, default=str)}") + + @tool( + "adopt_store", + "Take the agent's own store as this world's starting data, so the world holds what the " + "agent really has. Give the path to it, relative to the agent's source or absolute. Use " + "this whenever the agent ships or builds a store of its own: seeding it by hand instead " + "produces a smaller, invented dataset that its real queries were never written against.", + schema({"path": str, "note": str}, ["path"]), + ) + async def adopt_store(args: dict[str, Any]) -> dict[str, Any]: + given = str(args["path"]).strip() + found = Path(given) + if not found.is_absolute() and source_root: + found = Path(source_root) / given + if not found.exists() and source_root: + # An absolute path that is wrong is nearly always the agent's own repo-relative path + # read out of its source, so the same name under the real root is worth trying before + # reporting a miss. + under = Path(source_root) / Path(given).name + if under.exists(): + found = under + if not found.exists(): + return _err( + f"nothing at {found}.\n{_stores_here(source_root)}" + ) + if found.is_file() and found.stat().st_size == 0: + return _err( + f"{found} is empty, so there is nothing to adopt. If the agent builds or " + "downloads its store on first run, say so and ask rather than inventing data." + ) + try: + world.store.take(found) + except AttributeError: + return _err( + f"a {world.store.engine} store cannot take another one yet. Seed it instead, or " + "say what it would need." + ) + except Exception as raised: + return _err(f"could not take {found}: {type(raised).__name__}: {raised}") + state = world.state() + return _ok( + f"adopted {found.name}: " + + (", ".join(f"{name}: {len(rows)}" for name, rows in sorted(state.items())) or "nothing") + ) + + @tool( + "adopt_tool", + "Bind one tool to the agent's own implementation, so its code runs rather than a " + "replacement. Give the module and the callable. `style` is how it is invoked: " + "'function' for a plain function, 'staticmethod' for one hanging off a class, " + "'method' when an instance has to be built first. `first_arg` names what the agent's " + "state is passed as, if its signature takes it. The binding runs immediately, so give " + "`smoke_arguments` that a real record in this world would satisfy: an identifier that is " + "actually there. A smoke call that refuses proves the binding can refuse, not that it " + "works.", + schema( + { + "tool_name": str, + "module": str, + "callable": str, + "style": str, + "first_arg": str, + "factory": str, + "binding": str, + "smoke_arguments": dict, + }, + ["tool_name"], + ), + ) + async def adopt_tool(args: dict[str, Any]) -> dict[str, Any]: + name = str(args["tool_name"]) + if name not in contract.tool_names(): + return _err( + f"{name!r} is not a tool this agent has. It has: " + f"{', '.join(sorted(contract.tool_names()))}" + ) + if not source_root: + return _err( + "there is no agent source on disk to bind to, so nothing can be adopted here. " + "This agent was given as a specification rather than as code, so its tools have " + "to be written with define_handler." + ) + world.reach(source_root) + # A binding written here wins. The generated shapes cover a plain callable and a method + # on an object, which is most agents, but no set of shapes covers every framework, and + # guessing wrong is worse than letting whoever read the code write the two lines. + written = str(args.get("binding") or "").strip() + if written: + binding = written + elif not str(args.get("module") or ""): + return _err( + "give either a module and callable to bind to, or a binding of your own.\n\n" + + ADOPT_HELP + ) + else: + binding = _binding( + module=str(args["module"]), + called=str(args["callable"]), + style=str(args.get("style") or "function"), + first_arg=str(args.get("first_arg") or ""), + factory=str(args.get("factory") or ""), + ) + world.handlers[name] = binding + # Reverted after, because a smoke call against the agent's own code really does what the + # tool does: cancelling an order to prove the binding works would spend that order, and + # every scenario after it starts from this same world. Proving a tool works must not cost + # a record. + held = world.checkpoint() + call = world.call(name, args.get("smoke_arguments") or {}) + world.revert(held) + if call.refused: + return _ok( + f"{name} adopted. Its own code answered with a refusal, which is it working: " + f"{call.error}" + ) + if not call.ok: + del world.handlers[name] + tried[name] = tried.get(name, 0) + 1 + said = f"{name} not adopted, the binding failed: {call.error}" + # A name that does not exist is the commonest way this fails, and the answer to it is + # the list of names that do, not a repeat of the general advice. + if "nameerror" in (call.error or "").lower(): + said += f"\n\n{BINDING_SCOPE}" + if tried[name] >= 3: + said += ( + f"\n\nThat is {tried[name]} attempts at this one. Some tools cannot be " + "reached without editing the agent: a framework may build them inside a " + "session that does not exist here. Stop and say so, naming this tool and what " + "it would need, and let the person decide. A tool nobody can run is a fact " + "worth reporting, and writing a stand-in instead is the one failure that " + "leaves no trace." + ) + return _err(f"{said}\n\n{ADOPT_HELP}") + return _ok( + f"{name} adopted and ran, its own code. Returned {_brief(call.result)}" + ) + + @tool( + "run_tool", + "Call a defined tool and see what the world does. Use this to check a refusal works.", + schema({"tool_name": str, "arguments": dict}, ["tool_name"]), + ) + async def run_tool(args: dict[str, Any]) -> dict[str, Any]: + call = world.call(str(args["tool_name"]), args.get("arguments") or {}) + if call.refused: + return _ok(f"refused: {call.error}") + if not call.ok: + return _err(f"crashed: {call.error}") + return _ok(f"ok: {_brief(call.result)}") + + @tool( + "declare_sequence", + "Declare a series of calls whose end state should hold, so consistency across calls is " + "checked. Each call is {tool, arguments}. expect_state keys are 'table.column' or " + "'table.count'. Declaring the same name again replaces it.\n\n" + "Every sequence runs on its own from the frozen world: the state is put back before each " + "one, so they never see each other's rows and expect_state is an absolute count, not a " + "running total. If a sequence fails, the fault is in that sequence, not in the ones " + "declared before it.", + schema({"name": str, "calls": list, "expect_state": dict}, ["name", "calls"]), + ) + async def declare_sequence(args: dict[str, Any]) -> dict[str, Any]: + name = str(args.get("name") or f"sequence-{len(sequences)}") + calls = args.get("calls") or [] + + # Checked here rather than at save time. A malformed sequence that only fails three + # tools later reads as a mystery, and there is nothing to learn from it in between. + problems: list[str] = [] + if not calls: + problems.append("no calls: a sequence with no calls checks nothing") + for index, step in enumerate(calls): + if not isinstance(step, dict): + problems.append( + f"call {index} is not an object with a tool and arguments" + ) + continue + called = str(step.get("tool") or "") + if not called: + problems.append(f"call {index} has no tool name") + elif called not in world.handlers: + problems.append( + f"call {index} names {called!r}, which has no handler yet. Defined: " + f"{', '.join(sorted(world.handlers)) or 'none'}" + ) + if problems: + return _err(f"{name} not declared:\n - " + "\n - ".join(problems)) + + replaced = any(existing["name"] == name for existing in sequences) + sequences[:] = [existing for existing in sequences if existing["name"] != name] + sequences.append( + { + "name": name, + "calls": calls, + "expect_state": args.get("expect_state") or {}, + } + ) + verb = "replaced" if replaced else "declared" + return _ok( + f"{name} {verb}. {len(sequences)} sequences: {', '.join(s['name'] for s in sequences)}" + ) + + @tool( + "drop_sequence", + "Remove a declared sequence by name, or all of them with name '*'.", + {"name": str}, + ) + async def drop_sequence(args: dict[str, Any]) -> dict[str, Any]: + name = str(args.get("name") or "") + if name == "*": + sequences.clear() + return _ok("all sequences dropped") + before = len(sequences) + sequences[:] = [existing for existing in sequences if existing["name"] != name] + if len(sequences) == before: + return _err( + f"no sequence called {name!r}. Declared: " + f"{', '.join(s['name'] for s in sequences) or 'none'}" + ) + return _ok(f"{name} dropped. {len(sequences)} left") + + @tool( + "amend_contract", + "Let one of the agent's tools accept values it did not before. Use this when the world " + "holds something the agent has no way to name: an item added to the menu that item_id " + "does not list is dead data, and a scenario about it can only fail.\n\n" + "Only widen where the agent genuinely should accept the value. Say why in one line; it " + "is recorded on the contract, because a contract nobody can audit is worth nothing.", + {"tool_name": str, "argument": str, "values": list, "why": str}, + ) + async def amend_contract(args: dict[str, Any]) -> dict[str, Any]: + done, said = widen( + contract, + destination, + tool_name=str(args.get("tool_name") or ""), + argument=str(args.get("argument") or ""), + values=[str(value) for value in (args.get("values") or [])], + why=str(args.get("why") or ""), + ) + return _ok(said) if done else _err(said) + + @tool( + "cannot_reach_tool", + "Record that a tool's own implementation cannot be run here, so the world may implement " + "it instead. Only after adopt_tool has genuinely failed: say what stopped it, in one " + "line. The reason is written onto the contract permanently, because it is the only " + "record that this tool was a stand-in rather than the agent's own code.", + schema({"tool_name": str, "why": str}, ["tool_name", "why"]), + ) + async def cannot_reach_tool(args: dict[str, Any]) -> dict[str, Any]: + done, said = unreachable( + contract, + destination, + tool_name=str(args.get("tool_name") or ""), + why=str(args.get("why") or ""), + ) + return _ok(said) if done else _err(said) + + @tool( + "add_rule", + "Give the agent a hard rule its source did not state, when the operator asks for one. " + "The agent under test is told every rule and the judge grades against them, so this " + "changes what is being tested. Say why in one line; it is recorded on the contract.", + {"rule": str, "why": str}, + ) + async def add_rule_tool(args: dict[str, Any]) -> dict[str, Any]: + done, said = add_rule( + contract, + destination, + rule=str(args.get("rule") or ""), + why=str(args.get("why") or ""), + ) + return _ok(said) if done else _err(said) + + @tool( + "set_modality", + "Correct how a person actually reaches this agent: voice, chat or browser. Modality " + "picks the world, the simulated person and the transport, so a wrong one does not weaken " + "a run, it runs a different test. Use it when the operator says where the agent is " + "deployed and the contract disagrees: an agent's code reads the same answering a chat " + "window or a phone call, so where it is deployed is something only they can settle.", + {"modality": str, "why": str}, + ) + async def set_modality_tool(args: dict[str, Any]) -> dict[str, Any]: + done, said = set_modality( + contract, + destination, + modality=str(args.get("modality") or ""), + why=str(args.get("why") or ""), + ) + return _ok(said) if done else _err(said) + + @tool( + "inspect_world", + "Look at what is in the world you are building. With no collection named, lists what " + "there is and how much is in each. With one, returns records from it. `matching` is plain " + "text and filters to records containing it, which is how you find a record in a large " + "collection without reading all of it.", + schema({"table": str, "limit": int, "matching": str}, []), + ) + async def inspect_world(args: dict[str, Any]) -> dict[str, Any]: + state = world.state() + table = str(args.get("table") or "") + if not table: + return _ok( + "\n".join(f"{name}: {_size(held)}" for name, held in sorted(state.items())) + or "nothing in the world yet" + ) + if table not in state: + return _err( + f"nothing called {table!r}; there is {', '.join(sorted(state)) or 'nothing'}" + ) + held = state[table] + # A collection is a list of rows from a table, or a mapping the agent's own code keeps. + # Slicing the second one raises, so the shape is handled rather than assumed. + if isinstance(held, dict): + found = [{"_key": key, **value} if isinstance(value, dict) else {"_key": key, "value": value} + for key, value in held.items()] + elif isinstance(held, list): + found = list(held) + else: + found = [held] + matching = str(args.get("matching") or "").strip().lower() + if matching: + narrowed = [ + one for one in found if matching in json.dumps(one, default=str).lower() + ] + if not narrowed: + return _ok( + f"nothing in {table} contains {matching!r}, out of {len(found)} records." + ) + found = narrowed + shown = found[: int(args.get("limit") or 5)] + return _ok( + f"{len(found)} records" + + (f" matching {matching!r}" if matching else "") + + f", showing {len(shown)}:\n" + + "\n".join(_brief(one) for one in shown) + ) + + @tool( + "drop_rule", + "Take away a hard rule the agent does not really have. A rule nobody has is worse than " + "a missing one: the agent is told to obey it and graded for not doing something it was " + "never supposed to do. Say why.", + {"rule": str, "why": str}, + ) + async def drop_rule_tool(args: dict[str, Any]) -> dict[str, Any]: + done, said = drop_rule( + contract, + destination, + rule=str(args.get("rule") or ""), + why=str(args.get("why") or ""), + ) + return _ok(said) if done else _err(said) + + @tool( + "fix_tool", + "Correct a tool that was read wrong, or remove one the agent does not have. `args` " + "replaces its argument names in order; `arg_types` and `description` update those. Set " + "`remove` to take the tool away entirely. Everything downstream is built from these, so " + "a wrong argument name produces a world that refuses everything. Say why.", + schema( + { + "tool_name": str, + "args": list, + "arg_types": dict, + "description": str, + "remove": bool, + "why": str, + }, + ["tool_name", "why"], + ), + ) + async def fix_tool_tool(args: dict[str, Any]) -> dict[str, Any]: + done, said = fix_tool( + contract, + destination, + tool_name=str(args.get("tool_name") or ""), + why=str(args.get("why") or ""), + args=[str(a) for a in args["args"]] if args.get("args") else None, + arg_types={ + str(k): str(v) for k, v in (args.get("arg_types") or {}).items() + }, + description=str(args.get("description") or ""), + remove=bool(args.get("remove")), + ) + return _ok(said) if done else _err(said) + + @tool( + "write_simulator_prompt", + "Write the prompt that drives the simulated user of this agent, for a conversational " + "agent only. It is written once and every scenario fills its slots, so leave variables " + "as {{ instruction }}, {{ persona }} and any others this agent needs.\n\n" + "It has to cover how a person in this conversation actually behaves: that they are " + "living the situation rather than describing it, that they speak one turn at a time, " + "that they never break character or explain that they are testing anything, what they " + "know and when they may say it, and when the conversation is over. Write it for this " + "agent, not in general.", + schema({"prompt": str}, ["prompt"]), + ) + async def write_simulator_prompt(args: dict[str, Any]) -> dict[str, Any]: + prompt = str(args.get("prompt") or "") + problems = validate_simulator_prompt(prompt, require_persona=contract.conversational) + if problems: + return _err("Not saved:\n - " + "\n - ".join(problems)) + path = save_simulator_prompt(prompt, destination) + from ..simulator import variables_in + + return _ok( + f"Saved to {path}. Scenarios must fill: " + + ", ".join(sorted(variables_in(prompt))) + ) + + @tool( + "add_sub_goal", + "Add a named thing this agent can be checked on, shared by every scenario that needs " + "it. Defined here, once, so results roll up: the same sub-goal failing in seven of " + "twelve scenarios is one sentence.\n\n" + "`check` is Python: define check(world, calls) returning a sentence when something is " + "wrong, or None when it held. `world` is the environment afterwards; `calls` is every " + "tool call made, each with .name, .arguments, .ok and .refused — so a check can insist " + "a call happened with the right arguments, not merely that it happened.\n\n" + "Use `judged` only where nothing observable settles it, saying what a model has to " + "decide and why code cannot.", + schema( + {"name": str, "what": str, "check": str, "judged": str}, ["name", "what"] + ), + ) + async def add_sub_goal(args: dict[str, Any]) -> dict[str, Any]: + sub_goal = SubGoal( + name=str(args.get("name") or ""), + what=str(args.get("what") or ""), + check=str(args.get("check") or ""), + judged=str(args.get("judged") or ""), + ) + problems = validate_sub_goal(sub_goal) + if problems: + return _err("Not added:\n - " + "\n - ".join(problems)) + # Run it here, the same way a handler is run the moment it is defined. A check that raises + # is not a check, and accepting one now means every scenario that names it is refused later + # for a reason that looks like the scenario's fault rather than this one's. + if sub_goal.deterministic(): + outcome = run_check(sub_goal.check, world, list(world.calls), name=sub_goal.name) + if outcome.broken: + return _err( + f"Not added. {sub_goal.name} is not a working check: {outcome.said}\n\n" + f"{world.shapes()}\n\n" + "It does not have to hold against the world as it stands, since a sub-goal is " + "about what a run leaves behind. It does have to run without raising." + ) + catalogue.sub_goals = [ + one for one in catalogue.sub_goals if one.name != sub_goal.name + ] + catalogue.sub_goals.append(sub_goal) + save_catalogue(catalogue, destination) + settled = sum(1 for one in catalogue.sub_goals if one.deterministic()) + return _ok( + f"{sub_goal.name} added. The catalogue has {len(catalogue.sub_goals)}, " + f"{settled} settled by code: " + ", ".join(sorted(catalogue.names())) + ) + + @tool( + "write_env_file", + "Write one file the environment is built from: a Dockerfile, a compose file, a schema, an " + "entrypoint, whatever this agent needs. Paths are relative and stay inside the " + "environment directory. Call it once per file, then build with run_env_command.", + schema({"path": str, "contents": str}, ["path", "contents"]), + ) + async def write_env_file(args: dict[str, Any]) -> dict[str, Any]: + from .workspace import listing, write + + try: + written = write(destination, str(args["path"]), str(args["contents"])) + except ValueError as refused: + return _err(str(refused)) + lines = len(str(args["contents"]).splitlines()) + return _ok( + f"wrote {written.name}, {lines} lines. The environment now has: " + + ", ".join(listing(destination)) + ) + + @tool( + "run_env_command", + "Run one docker or docker compose command from the environment directory: build an image, " + "bring a store up, run something inside a container. Only container commands run here, so " + "anything the environment needs belongs in a file it builds from rather than in a " + "command. Returns the exit code and the output.", + schema({"command": str}, ["command"]), + ) + async def run_env_command(args: dict[str, Any]) -> dict[str, Any]: + from .workspace import run + + code, output = run(destination, str(args["command"])) + shown = output if len(output) <= 2500 else output[:1200] + "\n...\n" + output[-1200:] + if code != 0: + return _err(f"exit {code}\n{shown or '(no output)'}") + return _ok(f"ok\n{shown or '(no output)'}") + + @tool( + "write_store_ops", + "Teach the harness an engine it has never stood up: the image, the port it listens on, " + "the environment it needs to boot, and how to read, reset and change what it holds. " + "Only needed when the agent's engine is not one inspect_world already lists. Registering " + "it says nothing about whether it works: that is decided by proving it, not by either " + f"of us.\n\n{OPS_API}", + schema( + { + "engine": str, + "image": str, + "container_port": int, + "boot_env": dict, + "dsn_template": str, + "code": str, + }, + ["engine", "image", "container_port", "code"], + ), + ) + async def write_store_ops(args: dict[str, Any]) -> dict[str, Any]: + from .stores import StoreError, supported + from .stores.written import register_written + + try: + register_written( + engine=str(args["engine"]), + image=str(args["image"]), + container_port=int(args["container_port"]), + boot_env={str(k): str(v) for k, v in (args.get("boot_env") or {}).items()}, + dsn_template=str(args.get("dsn_template") or ""), + code=str(args["code"]), + ) + except (StoreError, SyntaxError, ValueError) as exc: + return _err(f"not registered: {exc}") + return _ok( + f"{args['engine']} registered, alongside {', '.join(supported())}. Whether its " + "reset is right is decided when the environment is proved, not now." + ) + + @tool( + "add_world_check", + "Add one check that decides whether this world is usable. Python defining " + "check(world) which returns None when it holds, or a sentence saying what is wrong. " + "`world.state()` gives every collection and its contents. It runs immediately, and it is " + "later put through a world that has been broken on purpose: a check that stays green " + "there is not checking anything.", + schema({"name": str, "code": str, "what": str}, ["name", "code"]), + ) + async def add_world_check(args: dict[str, Any]) -> dict[str, Any]: + name = str(args["name"]) + source = str(args["code"]) + outcome = run_world_check(source, world, name=name) + if outcome.broken: + return _err( + f"{name} is not a working check: {outcome.said}\n\n" + f"{_shapes(world)}\n\n{WORLD_CHECK_HELP}" + ) + world_checks[name] = source + held = "holds" if outcome.held else f"fails right now: {outcome.said}" + return _ok( + f"{name} added, {len(world_checks)} checks: {', '.join(sorted(world_checks))}.\n" + f"Against the world as it stands it {held}." + ) + + @tool( + "check_world", + "Exercise every tool with a valid call, a nonexistent id, and a missing argument, then " + "run the declared sequences. Reports what is wrong without saving anything.\n\n" + "Sequences are run independently from the frozen world, so a failure is never caused by " + "another sequence. Fix the failures it names; declaring more sequences only adds more " + "probes to pass.", + {}, + ) + async def check_world(_args: dict[str, Any]) -> dict[str, Any]: + report = probe(world, contract, sequences=sequences, kind=kind) + scores.append(report.score) + # Saying the score is going nowhere, rather than leaving it to be noticed. A stage that + # has misdiagnosed something will otherwise keep applying the same non-fix, and every + # round of that costs money and gets no closer. + stuck = "" + if len(scores) >= 3 and len(set(round(s, 2) for s in scores[-3:])) == 1: + stuck = ( + "\n\nThis is the third check with the same score. Whatever you are changing is " + "not what is failing. Read the failures above literally and fix one of them, or " + "say what you are stuck on." + ) + failing, cannot_fail, survived = _verified() + own = "" + if world_checks: + own = f"\n{len(world_checks) - len(failing)}/{len(world_checks)} of your own world checks hold" + if failing: + own += "\n failing: " + ", ".join(failing) + if cannot_fail: + own += ( + "\n these stayed green even with the world emptied and every tool " + "silenced, so they are not checking anything: " + + ", ".join(cannot_fail) + ) + # Said out loud, because the alternative is a person rewriting checks that were right. + for note in (survived or {}).get(UNDAMAGED, []): + own += ( + f"\n the emptied test could not be run: {note}. Nothing is concluded from " + "it, so this is ours to fix rather than yours." + ) + else: + own = "\nNo world checks of your own yet. Add them with add_world_check." + return _ok(f"{report.summary()}\nscore {report.score:.2f}{own}{stuck}") + + @tool( + "save_world", + "Freeze the world and write it out. Refused unless it passes its own checks.", + schema({"notes": str}, []), + ) + async def save_world(args: dict[str, Any]) -> dict[str, Any]: + report = probe(world, contract, sequences=sequences, kind=kind) + if report.score < ACCEPTABLE: + return _err( + f"Not saved, the world does not hold up yet.\n{report.summary()}\n" + f"score {report.score:.2f}, needs {ACCEPTABLE:.2f}" + ) + if not sequences: + return _err( + "Not saved. Declare at least one sequence first: a world whose calls each work " + "alone can still forget what the previous one did." + ) + # The world has to prove itself, and the proof has to be capable of failing. Both halves + # matter: checks nobody wrote verify nothing, and checks that pass a world with no data + # and no working tools verify nothing either. + if not world_checks: + return _err( + "Not saved. This world has no checks of its own yet. Add them with " + "add_world_check: what has to be true for this world to be worth testing " + "against, as code.\n\n" + WORLD_CHECK_HELP + ) + failing, cannot_fail, _survived = _verified() + if failing: + return _err( + "Not saved. These of your own world checks do not hold: " + + ", ".join(failing) + + ".\nFix the world, or the check if the check is what is wrong." + ) + if cannot_fail: + return _err( + "Not saved. These checks stayed green with the world emptied and every tool " + "silenced, so they are not verifying anything: " + + ", ".join(cannot_fail) + + ".\nA check has to inspect something that could actually be wrong. Make each " + "of them read the part of the world it claims to be about, and fail when it is " + "missing." + ) + # The environment is not only the world. Every scenario is a delta on what is built + # here, so a catalogue nobody wrote means every scenario invents its own wording and + # nothing rolls up across the suite. + if not catalogue.sub_goals: + return _err( + "Not saved. No sub-goals yet. They are defined here, once, and every scenario " + "names the ones it needs — that is what makes results add up across the suite. " + "Add them with add_sub_goal." + ) + settled = [one for one in catalogue.sub_goals if one.deterministic()] + if not settled: + return _err( + "Not saved. Every sub-goal is judged by a model. Most of what this agent does " + "leaves a trace in the world or in its calls, and those should be settled by " + "code; a judge is the fallback for what leaves none." + ) + if contract.conversational and not load_simulator_prompt(destination): + return _err( + "Not saved. This agent is conversational, so it needs a simulator prompt for " + "the person on the other side. Write it with write_simulator_prompt." + ) + if contract.conversational: + problems = validate_simulator_prompt( + load_simulator_prompt(destination), require_persona=True + ) + if problems: + return _err("Not saved. The simulator prompt is incomplete:\n - " + "\n - ".join(problems)) + dirty = dirty_state(world, sequences, kind) + if dirty: + counts = world.state() + listed = ", ".join(f"{name} ({len(counts[name])} rows)" for name in dirty) + return _err( + f"Not saved. These hold rows left over from building: {listed}.\n" + "This is the state every scenario starts from, so those rows would appear in " + "every test as somebody else's order already in the cart. Clear them with " + "change_data (DELETE FROM ...), keep the catalogue, and save again." + ) + # What the world publishes when something resets it. Without this a restored world + # announces no tools at all, so anything driving it through the environment interface + # sees an agent with nothing to call. + world.tools = [ + { + "name": spec.name, + "description": spec.description, + "parameters": { + arg: { + "type": spec.arg_types.get(arg, "str"), + "values": spec.arg_values.get(arg), + } + for arg in spec.args + }, + } + for spec in contract.tools + ] + path = save( + world, + destination, + notes=str(args.get("notes") or ""), + sequences=sequences, + # Written out with the world. They are judgement about this agent, and a world reopened + # without them would have to have them rewritten before it could be saved again. + world_checks=world_checks, + ) + tables = world.state() + return _ok( + f"Saved to {path}.\n" + f"{len(world.handlers)} tools, {len(tables)} collections, " + f"{sum(_size(held) for held in tables.values())} records, " + f"{len(world_checks)} world checks.\n" + f"score {report.score:.2f}" + ) + + server = create_sdk_mcp_server( + name=WORLD_SERVER, + version="0.1.0", + tools=[ + create_schema, + seed, + change_data, + define_handler, + run_tool, + declare_sequence, + drop_sequence, + amend_contract, + cannot_reach_tool, + add_rule_tool, + drop_rule_tool, + fix_tool_tool, + set_modality_tool, + inspect_world, + write_simulator_prompt, + add_sub_goal, + adopt_state, + adopt_store, + adopt_tool, + write_store_ops, + add_world_check, + write_env_file, + run_env_command, + check_world, + save_world, + ], + ) + return server, world + + +TOOL_NAMES = ( + "create_schema", + "seed", + "change_data", + "adopt_state", + "adopt_store", + "adopt_tool", + "define_handler", + "run_tool", + "declare_sequence", + "drop_sequence", + "amend_contract", + "cannot_reach_tool", + "add_rule", + "drop_rule", + "fix_tool", + "set_modality", + "inspect_world", + "write_simulator_prompt", + "add_sub_goal", + "write_store_ops", + "add_world_check", + "write_env_file", + "run_env_command", + "check_world", + "save_world", +) diff --git a/harness/src/agent_harness/world/workspace.py b/harness/src/agent_harness/world/workspace.py new file mode 100644 index 0000000..f282894 --- /dev/null +++ b/harness/src/agent_harness/world/workspace.py @@ -0,0 +1,122 @@ +"""Standing the environment up in containers, with the harness deciding what that means. + +The harness has read the agent's repository, so it knows what running that agent's code takes: +which base image, which install command, which store, which services. Encoding any of that here +would be guessing on behalf of an agent nobody has seen yet, and would be wrong for the next one. + +So this provides two things and no opinions: + +- a place to write files, under the session's own ``env`` directory +- a way to run container commands from there, and read back what happened + +Everything else, the Dockerfile, the compose file, the schema, the entrypoint, is written by +whoever read the repository. What is enforced is only what keeps this safe to run on somebody's +machine: files stay inside the environment directory, and the only commands that run are container +commands. +""" + +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +ENV = "env" + +# Only these. Not a general shell: a tool that can run anything is a tool with no guardrail, and +# the whole point of routing through here is that what happens is inspectable and bounded. +ALLOWED = ("docker", "docker-compose") + +# Long enough for an image build that downloads a base layer, short enough that a hung build is +# reported rather than waited on forever. +PATIENCE = 900 + + +def env_root(destination: Path) -> Path: + """Where this agent's environment definition lives, beside its world.""" + root = Path(destination) / ENV + root.mkdir(parents=True, exist_ok=True) + return root + + +def inside(destination: Path, path: str) -> Path: + """The full path for a file the harness wants to write, refused if it escapes. + + A path arrives as text from a model, so it is resolved and then checked rather than trusted. + Writing outside the environment directory would mean the harness could touch anything on the + machine it happens to be running on, which is not a thing to leave to a prompt. + """ + root = env_root(destination).resolve() + asked = (root / str(path).lstrip("/")).resolve() + if not asked.is_relative_to(root): + raise ValueError( + f"{path!r} is outside the environment directory. Everything the environment needs " + "lives under env/, so that building it cannot reach the rest of the machine." + ) + return asked + + +def write(destination: Path, path: str, contents: str) -> Path: + """Put one file into the environment definition.""" + target = inside(destination, path) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(contents, encoding="utf-8") + return target + + +def listing(destination: Path) -> list[str]: + root = env_root(destination) + return sorted( + str(found.relative_to(root)) for found in root.rglob("*") if found.is_file() + ) + + +def available() -> str: + """Why containers cannot be used here, or an empty string when they can.""" + if not shutil.which("docker"): + return "docker is not installed, or not on the path" + done = subprocess.run( + ["docker", "info", "--format", "{{.ServerVersion}}"], + capture_output=True, + text=True, + timeout=30, + ) + if done.returncode != 0: + return f"docker is installed but not running: {(done.stderr or '').strip()[:200]}" + return "" + + +def run(destination: Path, command: str, *, patience: int = PATIENCE) -> tuple[int, str]: + """Run one container command from the environment directory. + + Returns the exit code and the output, both streams together, because a build failure explains + itself across the two and reading only one is how the actual cause gets lost. + """ + words = command.split() + if not words: + return 1, "no command given" + if words[0] not in ALLOWED: + return 1, ( + f"{words[0]!r} is not something this can run. Only {' and '.join(ALLOWED)} commands, " + "because a general shell here would be a guardrail with nothing behind it. Everything " + "the environment needs should be in a file it builds from, not in a command." + ) + blocked = available() + if blocked: + return 1, blocked + try: + done = subprocess.run( + words, + cwd=str(env_root(destination)), + capture_output=True, + text=True, + timeout=patience, + ) + except subprocess.TimeoutExpired: + return 1, ( + f"gave up after {patience}s. An install that takes this long usually means a " + "dependency is being fetched that is not going to arrive; check what the last step " + "was trying to reach." + ) + output = ((done.stdout or "") + (done.stderr or "")).strip() + return done.returncode, output diff --git a/harness/tests/test_harness.py b/harness/tests/test_harness.py new file mode 100644 index 0000000..efa06ba --- /dev/null +++ b/harness/tests/test_harness.py @@ -0,0 +1,3463 @@ +"""Offline tests for the harness. No model calls, no network, no credentials. + +Every case here encodes something that must stay true for a generated environment to be +trustworthy: the contract cannot be structurally wrong, an unsupported agent source refuses +rather than half-works, and the submit gate returns its problems instead of writing a bad file. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from agent_harness import ( + AgentContract, + GitHubSource, + RepoSource, + SpecSource, + ToolSpec, + artifact_dir, + load_skill, + provider_env, + register_source, + resolve, + supported, + validate_contract, +) +from agent_harness.cli import build_parser +from agent_harness.scenario import Scenario +from agent_harness.session import ARTIFACT, DONE, TEXT, TOOL, Event +from agent_harness.tools import accept_contract, qualified +from agent_harness.understand import load, opening + + +def _contract(**overrides) -> AgentContract: + payload = { + "agent": "drive_thru", + "tools": [ToolSpec(name="order", args=["item_id"])], + "real_use_cases": ["order an item"], + } + payload.update(overrides) + return AgentContract(**payload) + + +# --- contract ------------------------------------------------------------------------ + + +def test_valid_contract_has_no_problems(): + assert validate_contract(_contract()) == [] + + +@pytest.mark.parametrize( + "overrides,expected", + [ + ({"agent": " "}, "empty:agent"), + ({"tools": []}, "no-tools"), + ({"real_use_cases": []}, "no-use-cases"), + ], +) +def test_validate_contract_catches_structural_problems(overrides, expected): + assert expected in validate_contract(_contract(**overrides)) + + +def test_duplicate_tool_names_are_rejected_and_named(): + """Names the offender: tool_names() is a set, so a naive length comparison never fires.""" + contract = _contract(tools=[ToolSpec(name="order"), ToolSpec(name="order")]) + assert "duplicate-tool-names:order" in validate_contract(contract) + + +def test_types_declared_for_arguments_that_do_not_exist_are_rejected(): + """A type on an argument the tool does not take means the reader misread the signature, + and a world built from it would be wrong in a way nothing downstream could detect.""" + contract = _contract( + tools=[ToolSpec(name="order", args=["item_id"], arg_types={"size": "str"})] + ) + assert "tool[order]:types-for-unknown-args:size" in validate_contract(contract) + + +def test_brief_carries_argument_types_into_downstream_prompts(): + contract = _contract( + tools=[ + ToolSpec( + name="remove_order_item", + args=["order_id"], + arg_types={"order_id": "list[str]"}, + ) + ] + ) + assert "remove_order_item(order_id: list[str])" in contract.brief() + + +def test_shapes_are_normalised_rather_than_rejected(): + """Benign shape variance is not a grounding error; rejecting it burns turns for nothing.""" + contract = AgentContract.model_validate( + { + "agent": "x", + "one_liner": ["a", "b"], + "hard_constraints": "only one rule", + "data_schema": [1, 2], + } + ) + assert contract.one_liner == "a\nb" + assert contract.hard_constraints == ["only one rule"] + assert contract.data_schema == {"value": [1, 2]} + + +# --- sources ------------------------------------------------------------------------- + + +def test_repo_and_spec_sources_are_registered(): + assert {"repo", "github", "spec"}.issubset(set(supported())) + + +def test_unsupported_source_refuses_and_names_what_exists(): + with pytest.raises(NotImplementedError) as raised: + resolve("browser", name="x") + assert "repo" in str(raised.value) + + +def test_repo_source_gets_read_tools_and_a_briefing_that_points_at_the_code(tmp_path): + source = RepoSource(name="a", root=tmp_path) + assert source.builtin_tools() == ("Read", "Glob", "Grep") + assert str(tmp_path) in source.briefing() + + +def test_github_source_reads_like_a_repository(tmp_path): + source = GitHubSource(name="a", root=tmp_path, url="https://github.com/acme/agent") + assert source.builtin_tools() == ("Read", "Glob", "Grep") + assert "https://github.com/acme/agent" in source.briefing() + + +def test_spec_source_gets_no_file_tools_because_there_is_nothing_to_read(): + source = SpecSource( + name="a", system_prompt="you are a bot", tool_schema=[{"name": "t"}] + ) + assert source.builtin_tools() == () + briefing = source.briefing() + assert "you are a bot" in briefing and "t" in briefing + + +def test_a_new_kind_of_agent_is_a_registration_not_a_code_change(): + register_source("fake", lambda **kw: RepoSource(name=kw["name"], root=".")) + assert resolve("fake", name="z").name == "z" + + +# --- session events ------------------------------------------------------------------ + + +@pytest.mark.parametrize( + "event,expected", + [ + (Event(TEXT, text="hello"), "hello"), + (Event(TOOL, tool="Read", detail={"target": "agent.py"}), " [Read agent.py]"), + (Event(TOOL, tool="Grep"), " [Grep]"), + ( + Event(ARTIFACT, detail={"path": "a/contract.json"}), + " [saved a/contract.json]", + ), + ], +) +def test_events_render_for_a_terminal(event, expected): + assert event.line() == expected + + +def test_done_event_reports_outcome_turns_and_spend(): + line = Event( + DONE, detail={"outcome": "success", "turns": 9, "cost_usd": 0.36} + ).line() + assert "success" in line and "turns=9" in line and "0.36" in line + + +# --- the submit gate ----------------------------------------------------------------- + + +def test_submit_writes_the_contract_when_it_is_valid(tmp_path): + result = accept_contract( + { + "agent": "drive_thru", + "tools": [{"name": "order", "args": ["item_id"]}], + "real_use_cases": ["order an item"], + }, + tmp_path, + ) + assert not result.get("is_error") + written = json.loads((tmp_path / "contract.json").read_text()) + assert written["agent"] == "drive_thru" + + +def test_submit_returns_problems_and_writes_nothing_when_invalid(tmp_path): + """The gate reports into the conversation so the next turn can fix it, which is the only + reason a bad contract does not reach disk.""" + result = accept_contract( + {"agent": "drive_thru", "tools": [], "real_use_cases": []}, tmp_path + ) + assert result.get("is_error") + text = result["content"][0]["text"] + assert "no-tools" in text and "no-use-cases" in text + assert not (tmp_path / "contract.json").exists() + + +def test_load_returns_none_when_the_stage_produced_nothing(tmp_path): + assert load(tmp_path) is None + + +# --- the world gate ------------------------------------------------------------------ + + +def _cart_world(): + from agent_harness.world import GeneratedWorld + + class W(GeneratedWorld): + name = "cart" + tools = [{"name": "add"}, {"name": "lst"}] + handlers = { + "add": ( + "def handle(args, db):\n" + " if 'item_id' not in args: raise ToolError('item_id is required')\n" + " m = db.one('SELECT * FROM menu WHERE id=?', [args['item_id']])\n" + " if not m: raise ToolError('no item %r' % args['item_id'])\n" + " db.execute('INSERT INTO cart (item_id) VALUES (?)', [args['item_id']])\n" + " return {'ok': 1}\n" + ), + "lst": "def handle(args, db):\n return db.query('SELECT * FROM cart')\n", + } + + world = W(":memory:") + world.connection.executescript( + "CREATE TABLE menu(id TEXT PRIMARY KEY); CREATE TABLE cart(item_id TEXT);" + ) + world.connection.execute("INSERT INTO menu VALUES ('big_mac')") + world.connection.commit() + contract = AgentContract( + agent="cart", + real_use_cases=["add an item"], + tools=[ + ToolSpec(name="add", args=["item_id"], arg_values={"item_id": ["big_mac"]}), + ToolSpec(name="lst"), + ], + ) + return world, contract + + +_SEQUENCE = [ + { + "name": "add-then-list", + "calls": [ + {"tool": "add", "arguments": {"item_id": "big_mac"}}, + {"tool": "lst", "arguments": {}}, + ], + "expect_state": {"cart.count": 1}, + } +] + + +def test_a_sound_world_passes_every_probe(): + from agent_harness.world import probe + + world, contract = _cart_world() + report = probe(world, contract, sequences=_SEQUENCE) + assert report.score == 1.0, report.summary() + + +def test_probing_leaves_the_world_exactly_as_it_found_it(): + """Probes mutate. Without reverting between them, each inherits the last one's debris and + a sequence expecting one row finds several, which reads as a bug in the world.""" + from agent_harness.world import probe + + world, contract = _cart_world() + probe(world, contract, sequences=_SEQUENCE) + assert world.state()["cart"] == [] + + +def test_probing_is_repeatable(): + from agent_harness.world import probe + + world, contract = _cart_world() + first = probe(world, contract, sequences=_SEQUENCE).score + second = probe(world, contract, sequences=_SEQUENCE).score + assert first == second == 1.0 + + +def test_a_tool_that_succeeds_on_a_nonexistent_id_fails_the_gate(): + """The defect the whole thing exists to catch: a call that should have been refused.""" + from agent_harness.world import probe + + world, contract = _cart_world() + world.handlers["add"] = ( + "def handle(args, db):\n" + " db.execute('INSERT INTO cart (item_id) VALUES (?)', [args.get('item_id')])\n" + " return {'ok': 1}\n" + ) + report = probe(world, contract, sequences=_SEQUENCE) + assert any("does not exist" in failure.detail for failure in report.failures), ( + report.summary() + ) + assert report.score < 0.85 + + +def test_a_crash_is_distinguished_from_a_refusal(): + from agent_harness.world import probe + + world, contract = _cart_world() + world.handlers["add"] = ( + "def handle(args, db):\n return {'id': args['item_id']}\n" + ) + report = probe(world, contract, sequences=_SEQUENCE) + assert any("crashed instead of refusing" in f.detail for f in report.failures) + + +def test_a_world_reverts_to_a_checkpoint(): + world, _ = _cart_world() + mark = world.checkpoint() + world.call("add", {"item_id": "big_mac"}) + assert len(world.state()["cart"]) == 1 + world.revert(mark) + assert world.state()["cart"] == [] + + +# --- wiring -------------------------------------------------------------------------- + + +def test_provider_env_pins_the_model_and_never_invents_a_project(monkeypatch): + monkeypatch.delenv("ANTHROPIC_VERTEX_PROJECT_ID", raising=False) + monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False) + env = provider_env("claude-sonnet-4-6") + assert env["CLAUDE_CODE_USE_VERTEX"] == "1" + assert env["ANTHROPIC_MODEL"] == "claude-sonnet-4-6" + assert "ANTHROPIC_VERTEX_PROJECT_ID" not in env + + +def test_qualified_tool_name_matches_the_mcp_convention(): + assert qualified("contract", "submit_contract") == "mcp__contract__submit_contract" + + +def test_the_skill_exists_and_forbids_guessing(): + text = load_skill("understand-agent") + assert "submit_contract" in text + assert "guess" in text.lower() + + +def test_artifacts_land_under_the_agent_name(): + assert artifact_dir("drive_thru").as_posix().endswith("sessions/drive_thru") + + +def test_cli_defaults_to_staying_open_for_corrections(): + args = build_parser().parse_args(["understand", "--name", "a", "--path", "."]) + assert args.interactive is True + assert ( + build_parser() + .parse_args(["understand", "--name", "a", "--path", ".", "--once"]) + .interactive + is False + ) + + +def test_opening_names_the_agent_and_asks_for_the_contract(tmp_path): + text = opening(RepoSource(name="drive_thru", root=tmp_path)) + assert "drive_thru" in text and "submit_contract" in text + + +# --- state expectations, shared by the gate and the grading -------------------------- + + +_STATE = {"orders": [{"id": "a", "item": "big_mac"}], "menu": [{"id": "big_mac"}]} + + +# --- scenarios ----------------------------------------------------------------------- + + +def _saved_world(tmp_path): + from agent_harness.world.snapshot import save + + world, contract = _cart_world() + save(world, tmp_path, notes="test world") + return tmp_path, contract + + +def _scenario(**overrides): + payload = { + "name": "orders-a-big-mac", + "tests": "the ordinary case", + "goal": "order a big mac", + "persona": "brisk", + "opening": "one big mac please", + "expect_state": {"cart.count": 1}, + } + payload.update(overrides) + return payload + + +# --- running and grading ------------------------------------------------------------- + + +def test_declared_types_become_something_a_tool_schema_can_carry(): + from agent_harness.run.targets import _python_type + + assert _python_type("list[str]") is list + assert _python_type("int") is int + assert _python_type("") is str + + +def test_the_agent_under_test_is_told_its_own_rules(): + from agent_harness.run.targets import agent_prompt + + _world, contract = _cart_world() + contract.hard_constraints = ["never substitute an item without asking"] + assert "never substitute" in agent_prompt(contract) + + +def test_the_cli_exposes_every_stage_and_one_conversation_across_them(): + parser = build_parser() + assert parser.parse_args(["scenarios", "--name", "a", "--count", "10"]).count == 10 + assert parser.parse_args(["run", "--name", "a"]).target == "local" + + +def test_talking_to_it_needs_nothing_on_the_command_line(): + """Which agent, where it lives and how many scenarios are all things you say.""" + parser = build_parser() + assert parser.parse_args(["chat"]).name is None + assert parser.parse_args(["chat"]).path is None + + +def test_a_conversation_resumes_at_whichever_stage_the_artifacts_reached(tmp_path): + from agent_harness.chat import BUILD, SCENARIOS, UNDERSTAND, open_conversation + + conversation = open_conversation(name="a", path=str(tmp_path), out=tmp_path) + assert conversation._resume_at() == UNDERSTAND + + accept_contract( + { + "agent": "a", + "real_use_cases": ["order"], + "tools": [{"name": "add", "args": ["item_id"]}], + }, + tmp_path, + ) + assert conversation._resume_at() == BUILD + + _saved_world(tmp_path) + assert conversation._resume_at() == SCENARIOS + + +def test_where_a_conversation_is_agrees_with_what_was_built(tmp_path): + from agent_harness.chat import SCENARIOS, open_conversation + + accept_contract( + { + "agent": "a", + "real_use_cases": ["order"], + "tools": [{"name": "add", "args": ["item_id"]}], + }, + tmp_path, + ) + _saved_world(tmp_path) + conversation = open_conversation(name="a", path=str(tmp_path), out=tmp_path) + assert conversation.stage_name == SCENARIOS + assert conversation.next_stage() is None + + +def test_a_conversation_with_no_agent_starts_by_asking_which_one(): + from agent_harness.chat import RECEPTION, open_conversation + + conversation = open_conversation() + assert conversation.source is None + assert conversation.stage_name == RECEPTION + assert conversation.next_stage() is None + + +def test_pointing_at_an_agent_settles_where_its_artifacts_go(tmp_path): + import asyncio + + from agent_harness.chat import UNDERSTAND, open_conversation + from agent_harness.sources import RepoSource + + conversation = open_conversation() + conversation._found["source"] = RepoSource(name="mine", root=tmp_path) + + async def _settle(): + # Reception is the only stage whose result is not a file, so the conversation reads it + # back rather than looking on disk. Advancing needs a live session, so only the + # settling half is exercised here. + settled = conversation._found.pop("source") + conversation.source = settled + conversation.out = conversation.out or artifact_dir(settled.name) + + asyncio.run(_settle()) + assert conversation.out.as_posix().endswith("sessions/mine") + assert conversation._resume_at() == UNDERSTAND + + +def test_pointing_at_somewhere_that_does_not_exist_is_refused(tmp_path): + from agent_harness.reception import point_at + + found = {} + refused = point_at("mine", str(tmp_path / "nope"), "repo", found) + assert refused["is_error"] and found == {} + + accepted = point_at("mine", str(tmp_path), "repo", found) + assert not accepted.get("is_error") + assert found["source"].name == "mine" + + +def test_pointing_at_a_github_url_clones_it_into_the_session(tmp_path, monkeypatch): + from agent_harness.reception import point_at + + called = {} + + def clone(command, **kwargs): + called["command"] = command + destination = Path(command[-1]) + destination.mkdir(parents=True) + return type("Completed", (), {"returncode": 0, "stderr": ""})() + + monkeypatch.setattr("agent_harness.sources.subprocess.run", clone) + found = {} + source_dir = tmp_path / "session" / "source" + accepted = point_at( + "demo-agent", + "https://github.com/acme/demo-agent", + "github", + found, + source_dir=source_dir, + ) + + assert not accepted.get("is_error") + assert called["command"][:4] == ["git", "clone", "--depth", "1"] + assert found["source"].root == source_dir + assert found["source"].kind == "github" + + +@pytest.mark.parametrize( + "url", + ["git@github.com:acme/demo-agent.git", "https://example.com/acme/demo-agent", "https://github.com/acme"], +) +def test_github_source_refuses_urls_that_cannot_be_public_https_clones(tmp_path, url): + from agent_harness.reception import point_at + + refused = point_at("demo-agent", url, "github", {}, source_dir=tmp_path / "source") + assert refused["is_error"] + + +def test_how_many_scenarios_is_something_you_say(): + from agent_harness.scenario_tools import TOOL_NAMES + + assert "aim_for" in TOOL_NAMES + + +# --- amending the contract ----------------------------------------------------------- + + +def _written_contract(tmp_path): + accept_contract( + { + "agent": "cart", + "real_use_cases": ["add an item"], + "tools": [ + { + "name": "add", + "args": ["item_id"], + "arg_values": {"item_id": ["big_mac"]}, + } + ], + }, + tmp_path, + ) + return load(tmp_path) + + +def test_the_agent_can_be_taught_a_value_it_did_not_accept(tmp_path): + """A world that gains an item the agent cannot name holds dead data, and every scenario + about it can only fail. The two have to move together.""" + from agent_harness.amend import widen + + contract = _written_contract(tmp_path) + done, said = widen( + contract, + tmp_path, + tool_name="add", + argument="item_id", + values=["mango_smoothie"], + why="added to the menu this morning", + ) + assert done, said + assert "mango_smoothie" in contract.tools[0].arg_values["item_id"] + # the stage's own copy and the file agree, or the stage checks against an action space + # that no longer exists + assert "mango_smoothie" in load(tmp_path).tools[0].arg_values["item_id"] + + +def test_an_amendment_is_recorded_rather_than_blended_in(tmp_path): + from agent_harness.amend import widen + + contract = _written_contract(tmp_path) + widen( + contract, + tmp_path, + tool_name="add", + argument="item_id", + values=["mango_smoothie"], + why="added to the menu this morning", + ) + recorded = load(tmp_path).amendments + assert len(recorded) == 1 + assert "mango_smoothie" in recorded[0] and "this morning" in recorded[0] + + +@pytest.mark.parametrize( + "overrides,expected", + [ + ({"tool_name": "nope"}, "is not a tool this agent has"), + ({"argument": "colour"}, "takes no argument"), + ({"why": " "}, "say why"), + ({"values": ["big_mac"]}, "already accepts"), + ], +) +def test_an_amendment_that_makes_no_sense_is_refused(tmp_path, overrides, expected): + from agent_harness.amend import widen + + contract = _written_contract(tmp_path) + call = { + "tool_name": "add", + "argument": "item_id", + "values": ["mango_smoothie"], + "why": "because", + } + call.update(overrides) + done, said = widen(contract, tmp_path, **call) + assert not done and expected in said + assert load(tmp_path).amendments == [] + + +# --- what a stage is allowed to do --------------------------------------------------- + + +def test_a_stage_may_use_nothing_it_was_not_given(): + """Deny by default, not deny-a-list. A session is offered whatever its host exposes, and an + allow-by-default gate let a host search tool through that cost a stage its whole budget.""" + import asyncio + + from agent_harness.config import permission_gate + + gate = permission_gate(granted=["Read", "Glob"]) + for refused in ("Write", "Edit", "Bash", "Task", "ToolSearch", "WebFetch"): + verdict = asyncio.run(gate(refused, {}, None)) + assert type(verdict).__name__ == "PermissionResultDeny" + assert "not part of this stage" in verdict.message + + allowed = asyncio.run(gate("Read", {"file_path": "a.py"}, None)) + assert type(allowed).__name__ == "PermissionResultAllow" + + +def test_a_question_still_reaches_the_operator(): + import asyncio + + from agent_harness.config import permission_gate + + asked = {} + + async def ask(tool_name, payload, _context): + asked["tool"] = tool_name + return "answered" + + assert asyncio.run(permission_gate(ask)("AskUserQuestion", {}, None)) == "answered" + assert asked["tool"] == "AskUserQuestion" + + +# --- the tools a stage actually publishes --------------------------------------------- + + +def _published(server): + """The tool names an in-process MCP server really exposes.""" + import asyncio + + from mcp.types import ListToolsRequest + + instance = server.get("instance") if isinstance(server, dict) else server + + async def ask(): + for key, handler in instance.request_handlers.items(): + if getattr(key, "__name__", "") == "ListToolsRequest": + result = await handler(ListToolsRequest(method="tools/list")) + return sorted(tool.name for tool in result.root.tools) + return [] + + return asyncio.run(ask()) + + +def test_every_stage_publishes_exactly_the_tools_it_claims(tmp_path): + """A tool listed in TOOL_NAMES but left out of the server is granted, named in error + messages, and does not exist. The model then hunts for it and works around the gate.""" + from agent_harness import scenario_tools as scenarios + from agent_harness.run import tools as runs + from agent_harness.world import tools as world + + root, contract = _saved_world(tmp_path) + server, _kept = scenarios.scenario_tools(contract, root, root, wanted=1) + assert _published(server) == sorted(scenarios.TOOL_NAMES) + + built, _world = world.world_tools(contract, root) + assert _published(built) == sorted(world.TOOL_NAMES) + + assert _published(runs.run_tools(root, root)) == sorted(runs.TOOL_NAMES) + + +def test_a_failed_call_is_not_reported_as_success(): + """A call that failed upstream still arrives with subtype "success", so reporting subtype + verbatim tells somebody their stage worked when nothing happened.""" + from agent_harness.session import _why_it_failed + + class Failed: + api_error_status = 400 + errors = ['{"error":"invalid_grant","error_subtype":"invalid_rapt"}'] + + said = _why_it_failed(Failed()) + assert "GOOGLE_APPLICATION_CREDENTIALS" in said and ".env.acceptance" in said + + class Other: + api_error_status = 529 + errors = ["overloaded"] + + assert "529" in _why_it_failed(Other()) + + +def test_the_credentials_in_play_are_said_out_loud(monkeypatch): + from agent_harness.config import credentials_hint + + monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS", "/keys/service-account.json") + assert credentials_hint() == "credentials: service-account.json" + + monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS") + assert "gcloud login" in credentials_hint() + + +def test_a_run_notices_when_it_was_billed_to_a_model_nobody_asked_for(): + """Asking for a model is not the same as getting one: the CLI has its own default, and a + request that quietly does not take shows up only on the invoice.""" + from claude_agent_sdk import ClaudeAgentOptions + + from agent_harness.session import Stage + + stage = Stage(ClaudeAgentOptions(model="claude-haiku-4-5"), name="s") + stage.models_used = {"claude-haiku-4-5-20251001"} + assert stage.unexpected_models() == set() + + stage.models_used = {"claude-opus-4-7"} + assert stage.unexpected_models() == {"claude-opus-4-7"} + + +def test_an_agent_already_built_can_be_reopened_without_its_repository(tmp_path): + """Coming back to fix a scenario should not mean pointing at the source again.""" + from agent_harness.chat import SCENARIOS, Conversation + + accept_contract( + { + "agent": "a", + "real_use_cases": ["order"], + "tools": [{"name": "add", "args": ["item_id"]}], + }, + tmp_path, + ) + _saved_world(tmp_path) + resumed = Conversation(source=None, out=tmp_path) + assert resumed.stage_name == SCENARIOS + + +def test_a_rule_the_source_never_stated_can_be_added_and_is_recorded(tmp_path): + """A hard constraint is told to the agent under test and graded by the judge, so adding one + changes what is being tested and has to be visible as ours rather than the agent's.""" + from agent_harness.amend import add_rule + + contract = _written_contract(tmp_path) + done, said = add_rule( + contract, + tmp_path, + rule="stays polite to customers", + why="asked for on the call", + ) + assert done and "graded from here on" in said + reloaded = load(tmp_path) + assert "stays polite to customers" in reloaded.hard_constraints + assert "rule added" in reloaded.amendments[0] and "polite" in reloaded.amendments[0] + + again, why = add_rule( + contract, tmp_path, rule="Stays Polite To Customers", why="again" + ) + assert not again and "already has that rule" in why + + unexplained, said = add_rule(contract, tmp_path, rule="be fast", why=" ") + assert not unexplained and "say why" in said + + +def test_a_collection_is_read_and_written_in_the_same_place(): + """A world can hold records in two places at once: a store the harness stood up, and the + state the agent's own code keeps, adopted whole. `state()` merges them and lets the store win + a name clash. The write path has to resolve a name the same way, or a scenario's setup + changes one copy while its checks read the other, and every run is graded against a world + that was never set up. Nothing else would show it: both copies exist and both look right.""" + from agent_harness.world.runtime import GeneratedWorld + from agent_harness.world.stores import open_store + + store = open_store("in_process") + store.start() + store.start_collection("orders", keyed=True) + store.add("orders", {"_id": "A1", "status": "pending", "who": "store"}) + + world = GeneratedWorld(store=store) + # The same collection name, in the agent's own state. Contrived, but this is exactly the + # shape an adopted agent with a container store beside it produces. + world.state_object = {"orders": {"A1": {"status": "pending", "who": "agent"}}} + + # The store wins the read... + assert world.state()["orders"][0]["who"] == "store" + # ...so it must win the write too. + world.change("orders", "A1", {"status": "cancelled"}, by="_id") + assert world.state()["orders"][0]["status"] == "cancelled" + # and the agent's own copy is untouched, rather than half the world moving. + assert world.state_object["orders"]["A1"]["status"] == "pending" + world.close() + + +def test_the_modality_can_be_corrected_when_the_source_reads_the_other_way(tmp_path): + """Modality picks the world, the simulated person and the transport, so a wrong one runs a + different test rather than a weaker one. It is also the field a source settles worst: an + agent's code reads the same answering a chat window or a phone call, so a repository that + looks like a text benchmark reads as text even when the operator has deployed it to a phone + number. Without this there was no correction short of running the whole stage again, which + reads the same source and reaches the same answer.""" + from agent_harness.amend import set_modality + + contract = _written_contract(tmp_path) + contract.modality = "chat" + + done, said = set_modality( + contract, tmp_path, modality="voice", why="deployed on Vapi, customers phone in" + ) + assert done and "voice" in said + reloaded = load(tmp_path) + assert reloaded.modality == "voice" + # Recorded as ours, like every other amendment, so the source and the correction stay apart. + assert any("modality chat -> voice" in one for one in reloaded.amendments) + + same, said = set_modality(contract, tmp_path, modality="voice", why="again") + assert not same and "already says voice" in said + + unknown, said = set_modality(contract, tmp_path, modality="telepathy", why="why not") + assert not unknown and "is not a modality" in said + + unexplained, said = set_modality(contract, tmp_path, modality="chat", why=" ") + assert not unexplained and "say why" in said + assert load(tmp_path).modality == "voice" + + +def test_a_rule_the_agent_does_not_have_can_be_taken_away(tmp_path): + """A rule nobody has is worse than a missing one: the agent is told to obey it and the + judge fails it for not doing something it was never supposed to do.""" + from agent_harness.amend import add_rule, drop_rule + + contract = _written_contract(tmp_path) + add_rule(contract, tmp_path, rule="never upsell", why="misread from a comment") + done, said = drop_rule( + contract, tmp_path, rule="upsell", why="the source never says that" + ) + assert done, said + assert load(tmp_path).hard_constraints == [] + assert "rule removed" in load(tmp_path).amendments[-1] + + missing, said = drop_rule(contract, tmp_path, rule="be nice", why="x") + assert not missing and "no rule like that" in said + + +def test_a_misread_tool_can_be_corrected(tmp_path): + """The most damaging thing stage one can get wrong: every argument name flows into the + handlers, the probes and the scenarios.""" + from agent_harness.amend import fix_tool + + contract = _written_contract(tmp_path) + done, said = fix_tool( + contract, + tmp_path, + tool_name="add", + args=["item_ids"], + why="the signature takes a list, singular was a misread", + ) + assert done, said + fixed = load(tmp_path).tools[0] + assert fixed.args == ["item_ids"] + # values recorded against the old name must not silently survive under a name nobody uses + assert "item_id" not in fixed.arg_values + assert "dropped values recorded for item_id" in said + + +def test_a_tool_the_agent_does_not_have_can_be_removed(tmp_path): + from agent_harness.amend import fix_tool + + contract = _written_contract(tmp_path) + contract.tools.append(ToolSpec(name="checkout", args=["id"])) + done, said = fix_tool( + contract, + tmp_path, + tool_name="checkout", + remove=True, + why="no such tool in the source", + ) + assert done and "1 tools left" in said + assert load(tmp_path).tool_names() == {"add"} + + +def test_correcting_a_contract_without_saying_why_is_refused(tmp_path): + from agent_harness.amend import drop_rule, fix_tool + + contract = _written_contract(tmp_path) + assert not fix_tool(contract, tmp_path, tool_name="add", args=["x"], why=" ")[0] + assert not drop_rule(contract, tmp_path, rule="anything", why="")[0] + + +def test_a_read_only_handler_does_not_poison_every_later_probe(): + """SQLite refuses to restore into a connection with a transaction open, and a handler that + only reads leaves one behind. Unsettled, the first such handler makes the world impossible + to check or save: "destination database is in use".""" + from agent_harness.world import probe + + world, contract = _cart_world() + # lst only queries, which is what leaves the read transaction open + world.call("lst", {}) + mark = world.checkpoint() + world.call("add", {"item_id": "big_mac"}) + world.call("lst", {}) + world.revert(mark) + assert world.state()["cart"] == [] + + report = probe(world, contract, sequences=_SEQUENCE) + assert report.score == 1.0, report.summary() + + +def test_a_row_put_in_wrong_can_be_taken_out_again(tmp_path): + """Seeding only inserts. Without a way to remove a row, the only way left to make a check + pass is to change the contract, which repairs the wrong thing.""" + import asyncio + + from agent_harness.world import tools as world_tools + + _root, contract = _saved_world(tmp_path) + server, world = world_tools.world_tools(contract, tmp_path) + assert "change_data" in world_tools.TOOL_NAMES + assert _published(server) == sorted(world_tools.TOOL_NAMES) + + world.connection.execute("INSERT INTO menu VALUES ('curry_sauce')") + world.connection.commit() + + async def call(name, payload): + from mcp.types import CallToolRequest, CallToolRequestParams + + instance = server.get("instance") if isinstance(server, dict) else server + for key, handler in instance.request_handlers.items(): + if getattr(key, "__name__", "") == "CallToolRequest": + result = await handler( + CallToolRequest( + method="tools/call", + params=CallToolRequestParams(name=name, arguments=payload), + ) + ) + return result.root.content[0].text + + said = asyncio.run( + call("change_data", {"sql": "DELETE FROM menu WHERE id='curry_sauce'"}) + ) + assert "1 rows changed" in said + assert not [row for row in world.state()["menu"] if row["id"] == "curry_sauce"] + + refused = asyncio.run(call("change_data", {"sql": "SELECT * FROM menu"})) + assert "UPDATE or DELETE" in refused + + +# --- the environment step: world, simulator prompt, sub-goal catalogue --------------- + + +def test_a_sub_goal_that_settles_nothing_is_rejected(): + """Every scenario referencing it would report a result nobody should believe.""" + from agent_harness.catalogue import SubGoal, validate_sub_goal + + assert validate_sub_goal(SubGoal(name="x", what="means something")) != [] + settled = SubGoal( + name="order-placed", + what="the order reached the system", + check="def check(world, calls):\n return None\n", + ) + assert validate_sub_goal(settled) == [] + assert settled.deterministic() + + judged = SubGoal( + name="polite", what="stayed polite", judged="nothing observable shows tone" + ) + assert validate_sub_goal(judged) == [] and not judged.deterministic() + + +def test_a_check_must_actually_define_one(): + from agent_harness.catalogue import SubGoal, validate_sub_goal + + problems = validate_sub_goal( + SubGoal(name="x", what="y", check="rows = world.state()['orders']") + ) + assert any("check(world, calls)" in problem for problem in problems) + + +def test_a_simulator_prompt_without_a_slot_runs_the_same_conversation_every_time(): + from agent_harness.simulator import fill, validate_simulator_prompt, variables_in + + fixed = ( + "You are a customer calling a drive-thru. Speak naturally, one turn at a time. " + * 2 + ) + assert any( + "no variables" in problem for problem in validate_simulator_prompt(fixed) + ) + + written = fixed + "\n\nWhat you want: {{ instruction }}\nWhat you know: {{ facts }}" + assert validate_simulator_prompt(written) == [] + assert variables_in(written) == {"instruction", "facts"} + + filled, missing = fill(written, {"instruction": "order a big mac"}) + assert "order a big mac" in filled and missing == ["facts"] + + +def test_a_conversational_simulator_prompt_requires_a_persona_slot(): + from agent_harness.simulator import validate_simulator_prompt + + prompt = "You are a customer. " * 10 + "\nWhat you want: {{ instruction }}" + + assert any("no persona slot" in problem for problem in validate_simulator_prompt(prompt, require_persona=True)) + assert validate_simulator_prompt(prompt + "\nWho you are: {{ persona }}", require_persona=True) == [] + + +def test_a_persona_is_a_structured_simulator_prompt_slot(): + from agent_harness.scenario import Persona, Scenario + from agent_harness.simulator import fill + + scenario = Scenario( + name="anxious-rider", + instruction="You need help finding your pickup point.", + persona=Persona( + name="Maya", + occupation="rider", + languages=["English", "Hindi"], + accent="South Asian English", + personality="anxious", + communication_style="direct and concise", + keywords=["in a noisy curbside area", "will ask for clarification"], + multilingual=True, + metadata={"pickup_context": "busy airport curb"}, + ), + ) + + filled, missing = fill("Caller:\n{{ persona }}\n\nNeed:\n{{ instruction }}", scenario.slots()) + + assert missing == [] + assert "Name: Maya" in filled + assert "Occupation: rider" in filled + assert "Personality: anxious" in filled + assert "Language(s): English, Hindi" in filled + assert "Accent: South Asian English" in filled + assert "Key Traits: in a noisy curbside area, will ask for clarification" in filled + assert "Pickup Context: busy airport curb" in filled + + +def test_an_empty_persona_is_rejected(): + from agent_harness.catalogue import Catalogue, SubGoal + from agent_harness.scenario import Persona, Scenario, validate_scenario + + scenario = Scenario( + name="empty-persona", + instruction="Place an order.", + persona=Persona(), + solution=[{"tool": "place", "arguments": {}}], + sub_goals=["placed"], + ) + catalogue = Catalogue(sub_goals=[SubGoal(name="placed", what="placed", judged="visible only to a judge")]) + + problems = validate_scenario(scenario, catalogue, {}, "{{ persona }}\n{{ instruction }}") + + assert "persona has no details" in problems + + +def test_a_persona_must_contain_the_profile_that_drives_variation(): + from agent_harness.catalogue import Catalogue, SubGoal + from agent_harness.scenario import Persona, Scenario, validate_scenario + + scenario = Scenario( + name="thin-persona", + instruction="Place an order.", + persona=Persona(name="Maya"), + solution=[{"tool": "place", "arguments": {}}], + sub_goals=["placed"], + ) + catalogue = Catalogue(sub_goals=[SubGoal(name="placed", what="placed", judged="visible only to a judge")]) + + problems = validate_scenario(scenario, catalogue, {}, "{{ persona }}\n{{ instruction }}") + + assert any("persona is incomplete" in problem for problem in problems) + assert all(field in problems[0] for field in ("personality", "languages", "accent")) + + +def test_a_check_that_raises_is_broken_not_failed(): + """A typo in an assertion must never read as a finding about the agent.""" + from agent_harness.checks import run_check + + world, _contract = _cart_world() + ok = run_check( + "def check(world, calls):\n return None\n", world, [], name="fine" + ) + assert ok.held and not ok.broken + + failed = run_check( + "def check(world, calls):\n return 'no rows'\n", world, [], name="says-why" + ) + assert not failed.held and not failed.broken and failed.said == "no rows" + + typo = run_check( + "def check(world, calls):\n return world.state()['nope'][0]\n", + world, + [], + name="typo", + ) + assert typo.broken and "KeyError" in typo.said + + +def test_a_check_can_insist_on_the_arguments_not_just_the_call(): + """Booking 10 PM when 11 PM was asked for is a failure, and detecting it is deterministic.""" + from agent_harness.checks import run_check + + world, _contract = _cart_world() + world.call("add", {"item_id": "big_mac"}) + source = ( + "def check(world, calls):\n" + " made = [c for c in calls if c.name == 'add']\n" + " if not made:\n return 'never added anything'\n" + " if made[0].arguments.get('item_id') != 'fries':\n" + " return 'added %r, expected fries' % made[0].arguments.get('item_id')\n" + " return None\n" + ) + outcome = run_check(source, world, world.calls, name="right-item") + assert not outcome.held and "expected fries" in outcome.said + + +# --- scenarios as deltas, and the two gates ------------------------------------------ + + +def _built_environment(tmp_path): + """A saved world plus a catalogue, which is what the environment step leaves behind.""" + from agent_harness.catalogue import Catalogue, SubGoal, save_catalogue + from agent_harness.world.snapshot import save + + world, contract = _cart_world() + save(world, tmp_path, notes="test", sequences=[]) + catalogue = Catalogue( + sub_goals=[ + SubGoal( + name="item-added", + what="the item reached the cart", + check=( + "def check(world, calls):\n" + " rows = world.state()['cart']\n" + " if len(rows) != 1: return '%d rows, expected 1' % len(rows)\n" + " return None\n" + ), + ), + SubGoal( + name="right-item", + what="the call carried the item that was asked for", + check=( + "def check(world, calls):\n" + " made = [c for c in calls if c.name == 'add' and c.ok]\n" + " if not made: return 'add was never called'\n" + " got = made[0].arguments.get('item_id')\n" + " return None if got == 'big_mac' else 'added %r' % got\n" + ), + ), + SubGoal(name="polite", what="stayed polite", judged="tone leaves no trace"), + ] + ) + save_catalogue(catalogue, tmp_path) + return tmp_path, contract, catalogue + + +def _delta(**overrides): + payload = { + "name": "adds-a-big-mac", + "use_case": "order an item", + "instruction": "Order one Big Mac.", + "solution": [{"tool": "add", "arguments": {"item_id": "big_mac"}}], + "sub_goals": ["item-added", "right-item"], + } + payload.update(overrides) + return payload + + +def test_a_scenario_is_proved_before_it_is_kept(tmp_path): + from agent_harness.scenario_tools import accept_scenario + + root, _contract, catalogue = _built_environment(tmp_path) + kept = [] + said = accept_scenario(_delta(), world_root=root, catalogue=catalogue, kept=kept) + assert not said.get("is_error"), said + assert "All three gates pass" in said["content"][0]["text"] + assert [one.name for one in kept] == ["adds-a-big-mac"] + + +def test_a_scenario_whose_solution_cannot_pass_its_own_checks_is_refused(tmp_path): + """Either the scenario is impossible or the checks are wrong. Both have happened.""" + from agent_harness.scenario_tools import accept_scenario + + root, _contract, catalogue = _built_environment(tmp_path) + said = accept_scenario( + _delta(solution=[{"tool": "add", "arguments": {"item_id": "sushi"}}]), + world_root=root, + catalogue=catalogue, + kept=[], + ) + assert said["is_error"] + text = said["content"][0]["text"] + assert "reference solution does not pass" in text + assert "refused by the world" in text and "sushi" in text + + +def test_a_scenario_whose_checks_pass_with_nothing_done_is_refused(tmp_path): + """A check that passes without the agent acting grades nothing while reporting a result.""" + from agent_harness.catalogue import SubGoal, save_catalogue + from agent_harness.scenario_tools import accept_scenario + + root, _contract, catalogue = _built_environment(tmp_path) + catalogue.sub_goals.append( + SubGoal( + name="always", + what="always true", + check="def check(world, calls):\n return None\n", + ) + ) + save_catalogue(catalogue, root) + said = accept_scenario( + _delta(sub_goals=["always"]), world_root=root, catalogue=catalogue, kept=[] + ) + assert said["is_error"] and "grade nothing" in said["content"][0]["text"] + + +def test_a_check_that_cannot_fail_without_calls_is_named_even_though_it_is_kept(tmp_path): + """A check comparing calls against rows holds when there are no calls at all, so it reports + itself as held for an agent that did nothing. The scenario is still graded by its other + checks, so it is kept, but sub-goals are shared and that one would roll up as a pass.""" + from agent_harness.catalogue import SubGoal, save_catalogue + from agent_harness.prove import prove + from agent_harness.scenario import Scenario + from agent_harness.scenario_tools import accept_scenario + + root, _contract, catalogue = _built_environment(tmp_path) + catalogue.sub_goals.append( + SubGoal( + name="quantity-respected", + what="as many rows as there were calls", + check=( + "def check(world, calls):\n" + " made = [c for c in calls if c.name == 'add' and c.ok]\n" + " rows = world.state()['cart']\n" + " if len(rows) != len(made):\n" + " return '%d calls, %d rows' % (len(made), len(rows))\n" + " return None\n" + ), + ) + ) + save_catalogue(catalogue, root) + delta = _delta(sub_goals=["item-added", "quantity-respected"]) + said = accept_scenario(delta, world_root=root, catalogue=catalogue, kept=[]) + text = said["content"][0]["text"] + + assert not said.get("is_error"), text + assert "All three gates pass" in text + assert "quantity-respected" in text and "held with nothing done" in text + proof = prove(Scenario(**delta), catalogue, root) + assert proof.holds and proof.weak == ["quantity-respected"] + + +def test_a_store_that_is_not_sqlite_needs_nothing_above_it_to_change(tmp_path): + """The harness writes the schema, the seed and the changes in whatever its agent's store + speaks. What it cannot do from a prompt is execute them, freeze the result and put it back, so + those are the only things a store owes the world. + + This registers a store that is not a database at all and drives the world through it: calls, + state, the scenario mutation vocabulary, freezing and restoring. Nothing above the store is + told which kind it is, which is the property that lets Postgres or ClickHouse drop in. + """ + import json + + from agent_harness.world.runtime import GeneratedWorld + from agent_harness.world.stores import open_store, register_store + + class Ledger: + """Records in a plain mapping, with its own statement language. Not SQL, deliberately.""" + + engine = "ledger" + key = "ledger" + + def __init__(self, database: str = "", **_extra): + self.held: dict[str, list[dict]] = {} + + def execute(self, statement: str, params=()) -> int: + verb, _, rest = statement.partition(" ") + if verb == "make": + self.held.setdefault(rest.strip(), []) + return 0 + if verb == "add": + name, _, body = rest.partition(" ") + self.held.setdefault(name, []).append(json.loads(body)) + return 1 + if verb == "clear": + name = rest.strip() + count = len(self.held.get(name) or []) + self.held[name] = [] + return count + raise ValueError(f"this store does not understand {verb!r}") + + def query(self, statement: str, params=()) -> list[dict]: + return list(self.held.get(statement.strip(), [])) + + def collections(self) -> list[str]: + return sorted(self.held) + + def records(self, collection: str) -> list[dict]: + return list(self.held.get(collection, [])) + + def holds(self, collection: str) -> bool: + return collection in self.held + + def add(self, collection: str, record) -> int: + self.held.setdefault(collection, []).append(dict(record)) + return 1 + + def amend(self, collection: str, key: str, changes, *, by: str = "") -> int: + changed = 0 + for row in self.held.get(collection, []): + if row.get(by or "order_id") == key: + row.update(dict(changes)) + changed += 1 + return changed + + def remove(self, collection: str, key: str = "", *, by: str = "") -> int: + rows = self.held.get(collection, []) + if not key: + self.held[collection] = [] + return len(rows) + kept = [r for r in rows if r.get(by or "order_id") != key] + self.held[collection] = kept + return len(rows) - len(kept) + + def freeze(self): + from agent_harness.world.stores import Snapshot + + return Snapshot(rows=json.loads(json.dumps(self.held))) + + def restore(self, snapshot) -> None: + self.held = {name: list(rows) for name, rows in snapshot.rows.items()} + + def save_to(self, path) -> None: + from pathlib import Path + + Path(path).mkdir(parents=True, exist_ok=True) + (Path(path) / "ledger.json").write_text(json.dumps(self.held), encoding="utf-8") + + def load_from(self, path) -> None: + from pathlib import Path + + self.held = json.loads((Path(path) / "ledger.json").read_text(encoding="utf-8")) + + def start(self) -> None: + return None + + def stop(self) -> None: + return None + + def dsn(self) -> str: + return "ledger://" + + def apply(self, script: str) -> None: + for line in script.splitlines(): + if line.strip(): + self.execute(line.strip()) + + def close(self) -> None: + return None + + register_store(Ledger.engine, Ledger) + world = GeneratedWorld(store=open_store("ledger")) + + # What the harness writes, in this store's own language rather than SQL. + world.store.execute("make orders") + world.store.execute('add orders {"order_id": "o1", "status": "pending"}') + assert world.state()["orders"] == [{"order_id": "o1", "status": "pending"}] + + # A tool runs against it through the same handler contract. + world.handlers["ship"] = ( + "def handle(args, db):\n" + " rows = db.query('orders')\n" + " if not rows:\n" + " raise ToolError('nothing to ship')\n" + " return rows[0]['order_id']\n" + ) + assert world.call("ship", {}).result == "o1" + + # Saving and loading, which is what every scenario depends on. + world.store.save_to(tmp_path) + world.store.execute("clear orders") + assert world.state()["orders"] == [] + world.store.load_from(tmp_path) + assert len(world.state()["orders"]) == 1 + + # And the same store going back in memory, which is what the gates use between probes. + kept = world.store.freeze() + world.store.execute("clear orders") + world.store.restore(kept) + assert len(world.state()["orders"]) == 1 + + # And the world's own vocabulary, which is what a scenario's setup uses. + world.put("orders", {"order_id": "o2", "status": "pending"}) + assert len(world.state()["orders"]) == 2 + assert world.drop("orders") == 2 + + # A refusal is still a refusal, with no store-specific handling anywhere. + refused = world.call("ship", {}) + assert refused.refused and not refused.ok + + +def test_a_world_check_that_cannot_fail_is_named(tmp_path): + """The environment's checks are written by whoever built it, so nothing independent confirms + they work. Breaking the world on purpose is that confirmation: a check that stays green + through a world with no data and no working tools is not verifying anything.""" + from agent_harness.checks import run_world_check + from agent_harness.world.mutate import blind, unnoticed + from agent_harness.world.runtime import GeneratedWorld + from agent_harness.world.snapshot import restore, save + + world = GeneratedWorld(":memory:") + world.connection.executescript( + "CREATE TABLE items (id TEXT); INSERT INTO items VALUES ('a');" + ) + world.connection.commit() + world.handlers["add"] = ( + "def handle(args, db):\n" + " db.execute('INSERT INTO items (id) VALUES (?)', [args['id']])\n" + " return 'added'\n" + ) + + real = "def check(world):\n return None if world.state()['items'] else 'no items'\n" + hollow = "def check(world):\n return None\n" + + save(world, tmp_path, sequences=[{"name": "x", "calls": []}]) + survived = unnoticed( + tmp_path, + [("real", real), ("hollow", hollow)], + run=lambda source, broken: run_world_check(source, broken, name="c"), + restore=restore, + ) + + # Emptying the world is what the real check is about, so it has to notice. + assert "real" not in survived["emptied"] + # And the one that inspects nothing survives every kind of damage, which is how it is caught. + assert blind(survived) == ["hollow"] + + +def test_the_world_keeps_the_checks_that_prove_it(tmp_path): + """A world reopened without them would have to have them rewritten before it could be saved + again, and they are judgement about this agent rather than anything a schema implies.""" + from agent_harness.world.runtime import GeneratedWorld + from agent_harness.world.snapshot import read_manifest, restore, save + + world = GeneratedWorld(":memory:") + world.connection.executescript("CREATE TABLE t (id TEXT); INSERT INTO t VALUES ('a');") + world.connection.commit() + written = {"holds": "def check(world):\n return None if world.state()['t'] else 'empty'\n"} + + save(world, tmp_path, sequences=[{"name": "s", "calls": []}], world_checks=written) + assert list(read_manifest(tmp_path).get("world_checks") or {}) == ["holds"] + # And a restored world can still be verified without rewriting them. + again = restore(tmp_path) + assert again.state()["t"] + + +def test_a_restored_world_still_knows_how_this_agent_says_no(tmp_path): + """Set at build time, read at run time, and those are different processes. + + Without it every refusal returned as a value is recorded as a success. A live call showed + what that costs: the agent's own lookup answered "Error: user not found" twice, the record + said both were fine, and the failure was then attributed to the agent re-calling a lookup + that had in fact never worked once. + """ + from agent_harness.world.runtime import GeneratedWorld + from agent_harness.world.snapshot import restore, save + + world = GeneratedWorld(":memory:") + world.refusal_signature = 'strings starting with "Error: "' + world.handlers = {"look": "def handle(args, db):\n return 'Error: user not found'\n"} + assert world.call("look").refused + + save(world, tmp_path, sequences=[]) + assert restore(tmp_path).call("look").refused + + +def _models_within(annotation): + """The pydantic models reachable from one field's type, through lists and optionals.""" + from pydantic import BaseModel + + if isinstance(annotation, type) and issubclass(annotation, BaseModel): + return [annotation] + found = [] + for inner in getattr(annotation, "__args__", ()): + found.extend(_models_within(inner)) + return found + + +def test_every_contract_field_is_advertised_to_the_model(tmp_path): + """A field the model cannot see is a field that never gets filled. + + The adoption fields are the case that matters: with `tool_entrypoints` empty the build stage + cannot tell that the agent ships its own tools, so it writes replacements and nothing + downstream shows the difference. The failure is silent, which is why this is checked by + reflection over the model rather than by remembering to keep a list in step. + """ + from pathlib import Path + + from agent_harness.contract import AgentContract + + from pydantic import BaseModel + + advertised = ( + Path(__file__).resolve().parents[1] / "src" / "agent_harness" / "tools.py" + ).read_text(encoding="utf-8") + # Set by the amendment tools during later stages, never by whoever submits the contract. + ours = {"amendments"} + + def named(model: type[BaseModel]) -> list[str]: + """Every field the model would have to fill, nested ones included. + + Nested, because that is where this went wrong the second time: the top-level field was + advertised and the shape underneath it was not, so the model filled in a store's kind and + never its host, port or seam. A field nobody can see is a field nobody fills. + """ + found: list[str] = [] + for name, field in model.model_fields.items(): + found.append(name) + for inner in _models_within(field.annotation): + found.extend(named(inner)) + return found + + missing = sorted( + { + name + for name in named(AgentContract) + if name not in ours and f'"{name}"' not in advertised + } + ) + assert not missing, f"submit_contract never mentions: {missing}" + + +def test_an_agent_inside_a_package_is_importable_from_its_package_root(tmp_path): + """Pointing at the part under test is the normal way to point at a packaged agent, and its + own imports resolve from the repository above it. Adding only the directory named makes every + such import fail as "No module named ", which reads as the package being absent + rather than as us having pointed at the middle of it, and blocks adoption entirely.""" + import sys + + from agent_harness.world.runtime import GeneratedWorld + + root = tmp_path / "repo" + inner = root / "agentpkg" / "envs" / "retail" + inner.mkdir(parents=True) + for package in (root / "agentpkg", root / "agentpkg" / "envs", inner): + (package / "__init__.py").write_text("", encoding="utf-8") + (inner / "data.py").write_text("def load():\n return {'orders': []}\n", encoding="utf-8") + + roots = GeneratedWorld._import_roots(str(inner)) + assert str(inner) in roots, "where it sits stays importable, for a flat agent" + assert str(root) in roots, "and the package root, or its own imports cannot resolve" + # Stops at the first directory that is not itself a package, the way Python does. + assert str(root.parent) not in roots + + kept = list(sys.path) + try: + GeneratedWorld(":memory:").reach(str(inner)) + loaded = __import__("agentpkg.envs.retail.data", fromlist=["load"]) + assert loaded.load() == {"orders": []} + finally: + sys.path[:] = kept + for name in [one for one in sys.modules if one.startswith("agentpkg")]: + del sys.modules[name] + + # An agent that is not in a package is unchanged: one directory, the one named. + plain = tmp_path / "flat" + plain.mkdir() + assert GeneratedWorld._import_roots(str(plain)) == [str(plain)] + assert GeneratedWorld._import_roots("") == [] + + +def test_the_adoption_fields_survive_the_write_path(tmp_path): + """Advertising them is half of it. They also have to reach the stage that acts on them.""" + from agent_harness.tools import accept_contract + from agent_harness.understand import load + + said = accept_contract( + { + "agent": "x", + "tools": [{"name": "t", "args": ["a"]}], + "real_use_cases": ["a plain sentence"], + "implementation": "present", + "tool_entrypoints": [ + { + "tool": "t", + "mode": "import", + "module": "pkg.mod", + "callable": "K.invoke", + "first_arg": "data", + } + ], + "refusal_signature": "a string beginning with Error:", + "data_store": {"kind": "in_process", "loaded_by": "pkg.data.load"}, + "runtime": {"language": "python", "install": "uv sync"}, + }, + tmp_path, + ) + assert not said.get("is_error"), said + + written = load(tmp_path) + assert written is not None + # The question the build stage actually asks before writing anything. + assert written.adoptable("t"), "the build stage would write a replacement instead" + assert written.refusal_signature + assert written.data_store and written.data_store.loaded_by == "pkg.data.load" + # And it has to be visible in the grounding block, or the stage never reads it. + assert "pkg.mod.K.invoke" in written.brief() + + +def test_a_scenario_naming_a_sub_goal_nobody_defined_is_refused(tmp_path): + from agent_harness.scenario_tools import accept_scenario + + root, _contract, catalogue = _built_environment(tmp_path) + said = accept_scenario( + _delta(sub_goals=["invented-here"]), + world_root=root, + catalogue=catalogue, + kept=[], + ) + assert said["is_error"] + assert "not in the catalogue" in said["content"][0]["text"] + + +def test_a_scenario_with_no_solution_cannot_be_proved(tmp_path): + from agent_harness.scenario_tools import accept_scenario + + root, _contract, catalogue = _built_environment(tmp_path) + said = accept_scenario( + _delta(solution=[]), world_root=root, catalogue=catalogue, kept=[] + ) + assert said["is_error"] and "no solution" in said["content"][0]["text"] + + +def test_a_suite_where_no_sub_goal_is_shared_does_not_roll_up(tmp_path): + """If a payment step appears in 50 scenarios, the results should say where payment fails.""" + from agent_harness.catalogue import Catalogue, SubGoal + from agent_harness.scenario import Scenario + from agent_harness.scenario_tools import not_ready + + catalogue = Catalogue( + sub_goals=[SubGoal(name=f"g{i}", what="x", judged="y") for i in range(4)] + ) + private = [ + Scenario(name=f"s{i}", instruction="do it", sub_goals=[f"g{i}"]) + for i in range(4) + ] + assert any("rolls up" in problem for problem in not_ready(private, 4, catalogue)) + + shared = [ + Scenario(name=f"s{i}", instruction="do it", sub_goals=["g0"]) for i in range(4) + ] + assert not_ready(shared, 4, catalogue) == [] + + +def test_the_simulator_prompt_slots_a_scenario_leaves_unfilled_are_caught(tmp_path): + from agent_harness.scenario import Scenario, validate_scenario + + root, _contract, catalogue = _built_environment(tmp_path) + prompt = ( + "You are a customer. " * 10 + + "\nWhat you want: {{ instruction }}\nAlso: {{ mood }}" + ) + scenario = Scenario.model_validate(_delta()) + problems = validate_scenario(scenario, catalogue, {"cart": [], "menu": []}, prompt) + assert any("mood" in problem for problem in problems) + + +# --- the voice webhook, answered by the world ---------------------------------------- + + +def test_a_hosted_agents_tool_call_is_answered_by_the_world(): + """The whole voice integration: a webhook, answered by running the call rather than by + looking up a canned response. A mock that always succeeds tells an agent it removed an item + that was never added.""" + import json + import urllib.request + + from agent_harness.run.voice import WorldWebhook + + world, _contract = _cart_world() + webhook = WorldWebhook().start() + try: + webhook.bind(world) + + def call(name, arguments): + body = json.dumps( + { + "message": { + "toolCalls": [ + { + "id": "call-1", + "function": {"name": name, "arguments": arguments}, + } + ] + } + } + ).encode() + request = urllib.request.Request( + f"http://127.0.0.1:{webhook.port}/tool", + data=body, + headers={"Content-Type": "application/json"}, + ) + with urllib.request.urlopen(request, timeout=5) as answer: + return json.loads(answer.read())["results"][0]["result"] + + assert "1" in call("add", {"item_id": "big_mac"}) + # the world really wrote the row, so a read-after-write flow is right + assert len(world.state()["cart"]) == 1 + + # and it can refuse, which a canned mock cannot + refused = call("add", {"item_id": "sushi"}) + assert "sushi" in refused + assert len(world.state()["cart"]) == 1 + + # the world answers for a tool the agent does not have, naming the ones it does + unknown = call("checkout", {}) + assert "no such tool" in unknown and "add" in unknown + # every call is recorded with its arguments, which is what grading reads + assert [c.name for c in webhook.calls] == ["add", "add", "checkout"] + finally: + webhook.stop() + + +def test_repointing_changes_only_where_the_agents_tools_are_answered(): + """The assistant's tools are the agent's — names, arguments and enums belong to whoever + built it. Redefining them would mean testing an agent we wrote.""" + from agent_harness.run.voice import pointed_at + + theirs = [ + { + "type": "function", + "function": { + "name": "order_combo_meal", + "parameters": { + "type": "object", + "properties": { + "meal_id": {"type": "string", "enum": ["combo_big_mac"]} + }, + "required": ["meal_id"], + }, + }, + "server": {"url": "https://dead-tunnel.example/tool"}, + } + ] + moved = pointed_at(theirs, "https://ours.example") + assert moved[0]["server"]["url"] == "https://ours.example/tool" + # everything else is untouched + assert moved[0]["function"] == theirs[0]["function"] + assert theirs[0]["server"]["url"] == "https://dead-tunnel.example/tool" + + +def test_a_scenario_fills_the_simulator_prompt_before_a_call_is_placed(tmp_path): + from agent_harness.simulator import save_simulator_prompt + from agent_harness.run.live import prepare + from agent_harness.scenario import Scenario + + root, _contract, _catalogue = _built_environment(tmp_path) + save_simulator_prompt( + "You are at the counter. " * 8 + "\nWhat you are here to do: {{ instruction }}", + root, + ) + world, instruction = prepare( + Scenario(name="s", instruction="Order one Big Mac."), root + ) + try: + assert "Order one Big Mac." in instruction + assert "{{" not in instruction + finally: + world.close() + + +def test_a_scenario_that_leaves_a_slot_empty_never_reaches_a_call(tmp_path): + """An unfilled slot would be read out to the caller verbatim.""" + import pytest as _pytest + + from agent_harness.simulator import save_simulator_prompt + from agent_harness.run.live import prepare + from agent_harness.scenario import Scenario + + root, _contract, _catalogue = _built_environment(tmp_path) + save_simulator_prompt( + "You are at the counter. " * 8 + "\nDo: {{ instruction }}\nMood: {{ mood }}", + root, + ) + with _pytest.raises(RuntimeError, match="mood"): + prepare(Scenario(name="s", instruction="Order one Big Mac."), root) + + +def test_a_live_run_is_refused_before_it_costs_anything(monkeypatch): + """Missing credentials must be caught up front. Discovering them after the world is + restored, the tunnel is up and the assistant is repointed wastes the expensive part and + reports a failure that says nothing about the agent.""" + from agent_harness.run.tools import missing_prerequisites + + monkeypatch.delenv("VAPI_API_KEY", raising=False) + monkeypatch.delenv("VAPI_ASSISTANT_ID", raising=False) + problems = missing_prerequisites() + assert any("VAPI_API_KEY" in problem for problem in problems) + + monkeypatch.setenv("VAPI_API_KEY", "x") + monkeypatch.setenv("VAPI_ASSISTANT_ID", "y") + monkeypatch.setenv("HARNESS_WEBHOOK_URL", "https://example.invalid") + assert missing_prerequisites() == [] + + +def test_running_is_a_stage_of_the_conversation(): + """Placing a call was the one step that could only be a command. If it drops out of the + stage order it silently becomes one again, and the chat ends at scenarios.""" + from agent_harness import chat + + assert chat._NEXT[chat.SCENARIOS] == chat.RUN + assert chat._NEXT[chat.RUN] == chat.DONE + + +def test_a_run_result_survives_being_written_and_read(tmp_path): + from agent_harness.checks import Outcome + from agent_harness.run.live import LiveRun + from agent_harness.run.tools import as_record, load_results, save_results + + run = LiveRun( + scenario="orders-a-big-mac", + settled=[Outcome("combo_placed", True), Outcome("no_extras", False, "added fries")], + judged=["explained_itself"], + calls=["order(...) -> ok"], + ) + record = as_record(run) + assert record["passed"] is False and record["met"] == 1 and record["of"] == 2 + + save_results([record], tmp_path) + assert load_results(tmp_path) == [record] + + +def test_a_tool_a_stage_was_not_given_is_denied_by_the_hook(): + """can_use_tool alone does not do this. An allowed_tools entry approves its tools before the + callback runs, and the SDK warns the callback is shadowed; a host ToolSearch reached every + stage, returned nothing and cost a turn. The PreToolUse hook is consulted for every call.""" + import asyncio + + from agent_harness.config import gate_hooks + + hooks = gate_hooks(["mcp__world__seed"]) + refuse = hooks["PreToolUse"][0].hooks[0] + + granted = asyncio.run(refuse({"tool_name": "mcp__world__seed"}, None, None)) + assert granted == {} + + asked = asyncio.run(refuse({"tool_name": "AskUserQuestion"}, None, None)) + assert asked == {} + + denied = asyncio.run(refuse({"tool_name": "ToolSearch"}, None, None)) + said = denied["hookSpecificOutput"] + assert said["permissionDecision"] == "deny" + assert "ToolSearch is not part of this stage" in said["permissionDecisionReason"] + assert "mcp__world__seed" in said["permissionDecisionReason"] + + +def test_every_stage_gates_with_the_hook_not_only_the_callback(): + """One stage left on the callback alone is one stage a host tool still reaches.""" + import inspect + + from agent_harness import build, reception, scenarios + from agent_harness.run import grade, stage, targets + + for module in (build, reception, scenarios, stage, targets, grade): + source = inspect.getsource(module) + if "permission_gate(" in source: + assert "gate_hooks(allowed)" in source, f"{module.__name__} has no hook gate" + + +def test_writing_new_results_keeps_the_ones_not_rerun(tmp_path): + """The live stage and the local suite share runs.json. Re-running one scenario must not + erase the record of another, whichever writer gets there second.""" + from agent_harness.run.tools import load_results, save_results + + save_results( + [{"scenario": "a", "passed": True}, {"scenario": "b", "passed": False}], tmp_path + ) + fresh = [r for r in load_results(tmp_path) if r.get("scenario") != "b"] + fresh.append({"scenario": "b", "passed": True, "transcript": "hello"}) + save_results(fresh, tmp_path) + + kept = {r["scenario"]: r for r in load_results(tmp_path)} + assert kept["a"]["passed"] is True + assert kept["b"]["passed"] is True and kept["b"]["transcript"] == "hello" + + +def test_submit_contract_schema_teaches_and_leaves_gating_to_the_gate(tmp_path): + """Every field marked required is rejected by the schema layer one at a time, a full model + turn each, before accept_contract can explain anything. Only the fields validate_contract + refuses to live without may be required; the rest are optional and gated with real messages.""" + import asyncio + + from mcp.types import ListToolsRequest + + from agent_harness.contract import MODALITIES + from agent_harness.tools import contract_tools + + server = contract_tools(tmp_path) + instance = server.get("instance") if isinstance(server, dict) else server + + async def schema_of(): + for key, handler in instance.request_handlers.items(): + if getattr(key, "__name__", "") == "ListToolsRequest": + result = await handler(ListToolsRequest(method="tools/list")) + return result.root.tools[0].inputSchema + return {} + + schema = asyncio.run(schema_of()) + # Nothing required at the schema layer: accept_contract is the only gate, and it reports + # every problem at once with what to do, which a JSON-schema rejection cannot. + # Nothing required: this layer runs before the tool body, so whatever it rejects never + # reaches the code that could have understood it. accept_contract is the single gate. + assert schema.get("required") == [] + assert "required" not in schema["properties"]["tools"]["items"] + # And the schema has to teach, not just validate — it is shown before the first call. + described = [ + name for name, spec in schema["properties"].items() if spec.get("description") + ] + assert len(described) >= 10, "properties must describe themselves" + assert schema["properties"]["modality"]["enum"] == list(MODALITIES) + assert schema["properties"]["tools"]["items"]["properties"]["arg_values"] + + +def test_a_bare_conversational_contract_is_nudged_once_then_accepted(tmp_path): + """No rules and no prompt excerpt on a conversational agent almost always means the prompt + was not found, so the first submission bounces with directions. The second goes through, + because a gate with no way past would permanently block an agent that genuinely has none.""" + import asyncio + + from agent_harness.tools import contract_tools + + server = contract_tools(tmp_path) + instance = server.get("instance") if isinstance(server, dict) else server + + async def call(payload): + from mcp.types import CallToolRequest, CallToolRequestParams + + for key, handler in instance.request_handlers.items(): + if getattr(key, "__name__", "") == "CallToolRequest": + request = CallToolRequest( + method="tools/call", + params=CallToolRequestParams(name="submit_contract", arguments=payload), + ) + answer = await handler(request) + return answer.root.content[0].text + + payload = { + "agent": "quiet", + "tools": [{"name": "act", "args": ["x"]}], + "real_use_cases": ["do the thing"], + } + first = asyncio.run(call(dict(payload))) + # Both thin spots are reported together, not one per turn. + assert "system_prompt_excerpt" in first and "data_schema" in first + assert "submit again" in first + assert not (tmp_path / "contract.json").exists() + + second = asyncio.run(call(dict(payload))) + assert "Accepted" in second + assert (tmp_path / "contract.json").exists() + + +def test_granting_a_tool_rebuilds_the_gate_not_just_the_list(tmp_path): + """The hook closes over the granted set when the stage is built, so appending to + allowed_tools alone leaves the new tool denied. grant() must rebuild all three.""" + import asyncio + + from claude_agent_sdk import ClaudeAgentOptions + + from agent_harness.config import gate_hooks + from agent_harness.session import Stage + + allowed = ["Read"] + options = ClaudeAgentOptions( + system_prompt="x", allowed_tools=allowed, permission_mode="default", + setting_sources=[], max_turns=1, + ) + options.hooks = gate_hooks(allowed) + stage = Stage(options, name="t") + stage.grant("flow", object(), ["hand_to_next_stage"]) + + assert "mcp__flow__hand_to_next_stage" in options.allowed_tools + refuse = options.hooks["PreToolUse"][0].hooks[0] + granted = asyncio.run(refuse({"tool_name": "mcp__flow__hand_to_next_stage"}, None, None)) + assert granted == {} + + +def test_handoff_is_refused_until_the_stage_has_its_artifact(tmp_path): + """Moving on is decided by code, from the artifacts, never by the model wanting to.""" + import asyncio + + from mcp.types import CallToolRequest, CallToolRequestParams + + from agent_harness.chat import Conversation + + conversation = Conversation(source=None, out=tmp_path, workspace=tmp_path) + conversation.stage_name = "understand" + server = conversation._flow_server() + instance = server.get("instance") if isinstance(server, dict) else server + + async def call(): + for key, handler in instance.request_handlers.items(): + if getattr(key, "__name__", "") == "CallToolRequest": + request = CallToolRequest( + method="tools/call", + params=CallToolRequestParams( + name="hand_to_next_stage", arguments={"request": "create the world"} + ), + ) + answer = await handler(request) + return answer.root.content[0].text + + said = asyncio.run(call()) + assert "not produced its artifact" in said + assert not conversation._handoff + + (tmp_path / "contract.json").write_text( + '{"agent": "a", "tools": [{"name": "t"}], "real_use_cases": ["u"]}' + ) + said = asyncio.run(call()) + assert "Handed over" in said + assert conversation._handoff["request"] == "create the world" + + +def test_every_problem_is_reported_at_once_with_what_to_do(tmp_path): + """Revealing the next problem only after the last is fixed costs a turn per problem and + reads as though the rules are being invented as it goes.""" + from agent_harness.tools import accept_contract + + result = accept_contract({"agent": "", "tools": [], "real_use_cases": []}, tmp_path) + said = result["content"][0]["text"] + assert result["is_error"] + # all three, in one answer + assert "empty:agent" in said and "no-tools" in said and "no-use-cases" in said + # and each carries what to do about it, not only its code + assert "artifact folder" in said and "real tools" in said + assert not (tmp_path / "contract.json").exists() + + +def test_a_contract_sent_inside_a_wrapper_is_unwrapped(tmp_path): + """A contract is a nested thing being described, so it arrives as {"contract": {...}} often + enough to matter. Every field is right; only the envelope is wrong, and rejecting that + teaches nothing while costing a turn.""" + from agent_harness.tools import accept_contract, unwrapped + + inner = { + "agent": "wrapped", + "tools": [{"name": "act", "args": ["x"]}], + "real_use_cases": ["do the thing"], + "hard_constraints": ["a rule"], + "system_prompt_excerpt": "you are a bot", + } + assert unwrapped({"contract": inner}) == inner + assert unwrapped(inner) == inner + # a real field that merely holds a dict must not be mistaken for an envelope + plain = {"agent": "x", "tools": [], "data_schema": {"agent": 1}} + assert unwrapped(plain) == plain + + result = accept_contract({"contract": inner}, tmp_path) + assert not result.get("is_error"), result["content"][0]["text"] + assert (tmp_path / "contract.json").exists() + + +@pytest.mark.parametrize( + "written", + [ + {"name": "order", "parameters": ["item_id", "size"]}, + {"name": "order", "arguments": ["item_id", "size"]}, + {"name": "order", "params": ["item_id", "size"]}, + {"name": "order", "arg_types": {"item_id": "str", "size": "str"}}, + {"name": "order", "parameters": {"item_id": "str", "size": "str"}}, + ], +) +def test_a_tool_written_with_a_synonym_still_records_its_arguments(written): + """args drives the handlers, the probes and every scenario. It is also the field most often + written under another name, and a contract bounced for a synonym costs a turn and teaches + nothing about the agent.""" + spec = ToolSpec.model_validate(written) + assert spec.args == ["item_id", "size"] + + +def test_a_tool_that_really_takes_nothing_stays_empty(): + """A tool genuinely taking no arguments is ordinary and must not be invented into one.""" + assert ToolSpec.model_validate({"name": "list_order_items"}).args == [] + + +def test_a_stringified_contract_is_parsed_rather_than_refused(tmp_path): + from agent_harness.tools import accept_contract, unwrapped + + inner = { + "agent": "stringy", + "tools": [{"name": "act", "args": ["x"]}], + "real_use_cases": ["do it"], + "hard_constraints": ["a rule"], + } + assert unwrapped({"contract": json.dumps(inner)}) == inner + assert unwrapped({"payload": json.dumps({"contract": inner})}) == inner + assert not accept_contract({"contract": json.dumps(inner)}, tmp_path).get("is_error") + + +def test_an_unrecognised_payload_is_told_what_arrived(tmp_path): + """Otherwise the answer is 'agent is empty, there are no tools' about a submission that + contained both, and the only way out is guessing at the packaging.""" + from agent_harness.tools import accept_contract + + said = accept_contract({"stuff": 1, "other": 2}, tmp_path)["content"][0]["text"] + assert "What arrived was: other, stuff" in said + assert "top-level arguments" in said + + +def test_a_skill_only_names_tools_its_stage_actually_has(): + """A SKILL.md is the method; the tools are the surface it is written against. They live in + different files, so a renamed tool leaves the skill telling the model to call something that + does not exist — and the model then hunts for it and works around the gate. Nothing else + catches that, because both halves are individually valid.""" + import re + + from agent_harness import scenario_tools + from agent_harness.config import SKILLS_ROOT + from agent_harness.run import tools as run_tools + from agent_harness.tools import CONTRACT_SERVER # noqa: F401 + from agent_harness.world import tools as world_tools + + surface = { + "understand-agent": {"submit_contract"}, + "build-environment": set(world_tools.TOOL_NAMES), + "write-scenarios": set(scenario_tools.TOOL_NAMES), + "run-scenarios": set(run_tools.TOOL_NAMES), + } + # A skill also backticks the names of fields it is telling the model to fill in. Those are + # not tools, and the list of them is derived rather than hand-kept so it cannot go stale. + from agent_harness.contract import AgentContract, ToolSpec + from agent_harness.catalogue import SubGoal + from agent_harness.scenario import Persona, Scenario + + fields = set() + for model in (AgentContract, ToolSpec, Scenario, Persona, SubGoal): + fields |= set(model.model_fields) + # Names from the check-writing examples the skills contain. + from agent_harness.contract import MODALITIES + + ignore = ( + fields + | set(MODALITIES) + | {"handle", "check", "args", "db", "world", "calls", "json", "ToolError"} + ) + + for stage, tools in surface.items(): + text = (SKILLS_ROOT / stage / "SKILL.md").read_text(encoding="utf-8") + # `name` or `name(` — the way a skill refers to a tool it wants called. + mentioned = set(re.findall(r"`([a-z_][a-z0-9_]*)\(?`", text)) + unknown = { + name + for name in mentioned - tools - ignore + if name not in {"hand_to_next_stage", "AskUserQuestion"} + } + assert not unknown, f"{stage}/SKILL.md names tools that do not exist: {sorted(unknown)}" + + +def test_a_contract_with_tools_but_no_data_is_nudged_once(tmp_path): + """The world is built from data_schema and base_environment. Without them the build stage has + no schema to create and no rows to seed, so every tool call it makes refuses — and that looks + like a strict world rather than an empty one.""" + import asyncio + + from agent_harness.tools import contract_tools + + server = contract_tools(tmp_path) + instance = server.get("instance") if isinstance(server, dict) else server + + async def call(payload): + from mcp.types import CallToolRequest, CallToolRequestParams + + for key, handler in instance.request_handlers.items(): + if getattr(key, "__name__", "") == "CallToolRequest": + answer = await handler( + CallToolRequest( + method="tools/call", + params=CallToolRequestParams( + name="submit_contract", arguments=payload + ), + ) + ) + return answer.root.content[0].text + + payload = { + "agent": "dataless", + "tools": [{"name": "act", "args": ["x"]}], + "real_use_cases": ["do the thing"], + "hard_constraints": ["a rule"], + "system_prompt_excerpt": "you are a bot", + } + first = asyncio.run(call(dict(payload))) + assert "data_schema" in first and "submit again" in first + assert not (tmp_path / "contract.json").exists() + + second = asyncio.run(call(dict(payload))) + assert "Accepted" in second + + assert (tmp_path / "contract.json").exists() + + +def test_only_a_tool_that_says_it_saved_reports_an_artifact(): + """Matching any path-shaped token in any result meant reading a file announced itself as an + artifact: the stage looks like it is producing output while it is still only looking around, + and a front end reloads its panes on every read.""" + from dataclasses import dataclass + + from agent_harness.session import _saved_path + + @dataclass + class Block: + content: object + is_error: bool = False + + # a read + assert _saved_path(Block(" 1\timport json\n 2\tfrom pathlib import Path")) == "" + assert _saved_path(Block("/some/agent/envs/retail/__init__.py")) == "" + # a write + assert _saved_path(Block("Accepted and saved to out/contract.json.")) == "out/contract.json" + assert _saved_path(Block("Saved 3 scenarios to out/scenarios.json.")) == "out/scenarios.json" + # list-shaped content, as the SDK sometimes gives it + assert ( + _saved_path(Block([{"text": "Saved to artifacts/x/world.sqlite"}])) + == "artifacts/x/world.sqlite" + ) + + +@pytest.mark.parametrize( + "written,field,expected", + [ + ({"use_cases": ["a"]}, "real_use_cases", ["a"]), + ({"scenarios": ["a"]}, "real_use_cases", ["a"]), + ({"rules": ["r"]}, "hard_constraints", ["r"]), + ({"constraints": ["r"]}, "hard_constraints", ["r"]), + ({"system_prompt": "p"}, "system_prompt_excerpt", "p"), + ({"instructions": "p"}, "system_prompt_excerpt", "p"), + ({"schema": {"a": 1}}, "data_schema", {"a": 1}), + ({"seed_data": {"t": []}}, "base_environment", {"t": []}), + ], +) +def test_a_field_written_under_the_obvious_name_still_lands(written, field, expected): + """Every one of these was written by a model that had read the schema and still reached for + the more obvious word. Bouncing it produces a loop: the answer to `use_cases` was + 'no-use-cases', which reads as missing rather than misnamed, so the same submission comes + back with the shape changed and the name untouched.""" + contract = AgentContract.model_validate({"agent": "x", **written}) # our name already set + assert getattr(contract, field) == expected + + +def test_the_agent_name_can_arrive_as_name(): + assert AgentContract.model_validate({"name": "bot", "tools": []}).agent == "bot" + + +def test_our_own_name_wins_when_both_are_given(): + contract = AgentContract.model_validate( + {"agent": "x", "real_use_cases": ["ours"], "use_cases": ["theirs"]} + ) + assert contract.real_use_cases == ["ours"] + + +def test_the_gate_names_the_field_it_wants(tmp_path): + """A code alone cannot be acted on when the mistake is the field's name.""" + from agent_harness.tools import accept_contract + + said = accept_contract({"agent": "x", "tools": [], "real_use_cases": []}, tmp_path) + text = said["content"][0]["text"] + assert "`real_use_cases`" in text and "not `use_cases`" in text + assert "`tools`" in text + + +# --- scenario folders and the ready gate --------------------------------------------- + + +def test_the_ready_gate_refuses_a_scenario_whose_world_was_never_set_up(tmp_path): + """The precondition gate. A scenario about the last five items is only a test of the agent + if there really are five; otherwise the agent fails for something we got wrong, and it reads + as the agent's fault.""" + from agent_harness.prove import prove + + root, _contract, catalogue = _built_environment(tmp_path) + scenario = Scenario.model_validate( + _delta( + ready_code=( + "def ready(world):\n" + " rows = world.state()['cart']\n" + " return None if rows else 'the cart is empty; this scenario needs one item'\n" + ) + ) + ) + proof = prove(scenario, catalogue, root) + assert not proof.ready + assert not proof.holds + assert "the cart is empty" in proof.why() + assert "test us rather than the agent" in proof.why() + assert proof.gates() == {"ready": False, "solvable": False, "not_vacuous": False} + + +def test_setup_code_makes_the_world_the_scenario_presumes(tmp_path): + """setup runs, then ready confirms it worked, and only then is anything else asked.""" + from agent_harness.prove import prove + + root, _contract, catalogue = _built_environment(tmp_path) + scenario = Scenario.model_validate( + _delta( + setup_code=( + "def setup(world):\n" + " world.connection.execute(\"INSERT INTO menu (id) VALUES ('sushi')\")\n" + " world.connection.commit()\n" + ), + ready_code=( + "def ready(world):\n" + " ids = [r['id'] for r in world.state()['menu']]\n" + " return None if 'sushi' in ids else 'sushi was never added to the menu'\n" + ), + ) + ) + proof = prove(scenario, catalogue, root) + assert proof.ready and proof.holds, proof.why() + + +def test_the_setups_own_calls_are_not_credited_to_the_agent(tmp_path): + """A check that counts calls must not see the ones the scenario made on its own behalf.""" + from agent_harness.prove import prepared + + root, _contract, _catalogue = _built_environment(tmp_path) + scenario = Scenario.model_validate( + _delta( + setup_code=( + "def setup(world):\n" + " world.call('add', {'item_id': 'big_mac'})\n" + ) + ) + ) + world, applied, ready = prepared(scenario, root) + try: + assert applied.ok and ready.ok + assert len(world.state()["cart"]) == 1, "the setup should have acted" + assert world.calls == [], "but its calls are not the agent's" + finally: + world.close() + + +def test_broken_setup_is_ours_and_says_so(tmp_path): + from agent_harness.prove import prove + + root, _contract, catalogue = _built_environment(tmp_path) + scenario = Scenario.model_validate(_delta(setup_code="def setup(world):\n world.nope()\n")) + proof = prove(scenario, catalogue, root) + assert not proof.ready + assert proof.broken, "a setup that raises is our mistake, not a failing scenario" + assert "AttributeError" in proof.why_not_ready + + + + +def test_a_kept_scenario_becomes_a_folder_of_files(tmp_path): + """The files are the artifact, not a rendering of one. Something you can open and run is + something you can argue with.""" + from agent_harness.folder import folder_for, read_folder + from agent_harness.scenario_tools import write_scenarios + + root, _contract, catalogue = _built_environment(tmp_path) + scenario = Scenario.model_validate( + _delta( + setup_code="def setup(world):\n pass\n", + ready_code="def ready(world):\n return None\n", + ) + ) + index = write_scenarios([scenario], root, catalogue) + + here = folder_for(root, scenario.name) + assert (here / "scenario.json").exists() + assert (here / "setup.py").exists() + assert (here / "ready.py").exists() + # One file per deterministic sub-goal; the judged one has no check to write. + assert sorted(p.name for p in (here / "checks").iterdir()) == [ + "item-added.py", + "right-item.py", + ] + assert index.name == "scenarios.json" + + # The code lives in the files, not duplicated into the JSON, so the two cannot drift. + body = json.loads((here / "scenario.json").read_text()) + assert "setup_code" not in body and "ready_code" not in body + + # And it reads back whole. + again = read_folder(root, scenario.name) + assert again is not None + assert again.setup_code.strip() == "def setup(world):\n pass" + assert again.solution == scenario.solution + + +def test_a_check_file_runs_on_its_own_and_agrees_with_the_harness(tmp_path): + """The same file, the same answer, whether the harness runs it or a person does. If those + two could disagree, neither could be trusted.""" + import subprocess + import sys + + from agent_harness.folder import folder_for, write_folder + from agent_harness.prove import prepared + + root, _contract, catalogue = _built_environment(tmp_path) + scenario = Scenario.model_validate(_delta()) + write_folder(scenario, catalogue, root) + + # Leave the world in the state a passing run would have left it in. + world, _applied, _ready = prepared(scenario, root) + try: + world.call("add", {"item_id": "big_mac"}) + finally: + world.close() + + check_file = folder_for(root, scenario.name) / "checks" / "item-added.py" + done = subprocess.run( + [sys.executable, str(check_file), str(root / "world.sqlite")], + capture_output=True, + text=True, + timeout=60, + ) + # The world on disk is the base world, which has an empty cart, so this check should fail — + # and the point is that it says so rather than erroring. + assert done.returncode in (0, 1), done.stderr[-400:] + assert "held" in done.stdout or "FAILED" in done.stdout, done.stdout + done.stderr[-300:] + + +def test_every_stage_is_told_what_the_harness_is_for(): + """A stage that knows only its own step does its step well and still gets the point of it + wrong: it works around a gate instead of fixing what the gate named, or it reports a number + that quietly skipped half its checks.""" + for stage in ("understand-agent", "build-environment", "write-scenarios", "run-scenarios"): + text = load_skill(stage) + assert text.startswith("# The harness"), stage + assert "# The stage you are in now" in text, stage + # the ideas a stage must not be able to miss + assert "Code decides what is true" in text, stage + assert "refusal" in text and "crash" in text, stage + + +# --- sessions: one conversation, one folder ------------------------------------------- + + +def test_a_session_is_a_folder_that_knows_what_it_holds(tmp_path): + """Nothing is held in memory that is not also on disk, so closing the page, restarting the + server or coming back tomorrow all resume by reading the folder.""" + from agent_harness import sessions + + one = sessions.create(agent="drive_thru", source="/somewhere/agent", base=tmp_path) + assert one.id.startswith("drive-thru-") + assert (one.path / "session.json").exists() + + has = one.has() + assert has == { + "contract": False, "world": False, "simulator_prompt": False, + "sub_goals": 0, "scenarios": 0, "validated": None, + "runs": 0, "runs_passed": 0, "messages": 0, + } + + again = sessions.load(one.id, tmp_path) + assert again is not None + assert again.agent == "drive_thru" and again.source == "/somewhere/agent" + + +def test_two_goes_at_the_same_agent_are_two_sessions(tmp_path): + from agent_harness import sessions + + first = sessions.create(agent="same", base=tmp_path) + second = sessions.create(agent="same", base=tmp_path) + assert first.id != second.id + assert {one.id for one in sessions.every(tmp_path)} == {first.id, second.id} + + +def test_the_conversation_is_kept_in_the_session_folder(tmp_path): + """A refresh must not lose what was said.""" + from agent_harness import sessions + + one = sessions.create(agent="talky", base=tmp_path) + sessions.remember(one.path, sessions.Message(role="you", text="hello", stage="reception")) + sessions.remember( + one.path, + sessions.Message( + role="harness", text="hi", stage="reception", + tools=[{"label": "point at agent", "said": ["Pointed at talky"]}], + ), + ) + said = sessions.history(one.path) + assert [m["role"] for m in said] == ["you", "harness"] + assert said[1]["tools"][0]["label"] == "point at agent" + assert one.has()["messages"] == 2 + + # A half-written final line is what a killed process leaves; it must not take the rest. + with (one.path / "chat.jsonl").open("a", encoding="utf-8") as file: + file.write('{"role": "you", "text": "cut off') + assert len(sessions.history(one.path)) == 2 + + +def test_deleting_a_session_will_not_reach_outside_the_sessions_root(tmp_path): + """A mistyped id must never take anything else with it.""" + from agent_harness import sessions + + one = sessions.create(agent="doomed", base=tmp_path) + outsider = tmp_path.parent / "not-a-session" + outsider.mkdir(exist_ok=True) + + assert sessions.remove("../not-a-session", tmp_path) is False + assert outsider.exists() + assert sessions.remove("no-such-session", tmp_path) is False + + assert sessions.remove(one.id, tmp_path) is True + assert not one.path.exists() + + +def test_any_stage_whose_input_exists_can_be_opened(tmp_path): + """Stages are not a wizard. Coming back to correct a contract after the world is built is + the ordinary case, so what cannot be skipped is the input, not the order.""" + from agent_harness.chat import Conversation + from agent_harness.tools import accept_contract + + empty = Conversation(out=tmp_path) + blocked = empty.reachable() + assert blocked["reception"] == "" + assert "where its source lives" in blocked["understand"] + assert "needs a contract" in blocked["build"] + # Without a contract, every later stage says so — not "needs a world", which would send + # somebody to build one against nothing. + assert "needs a contract" in blocked["scenarios"] + assert "needs a contract" in blocked["run"] + + root, contract, _catalogue = _built_environment(tmp_path / "built") + accept_contract(contract.model_dump(), root) + ready = Conversation(out=root) + open_now = ready.reachable() + assert open_now["build"] == "", open_now + assert open_now["scenarios"] == "", open_now + assert "needs scenarios" in open_now["run"] + + +def test_reception_can_hand_over_in_the_turn_that_finds_the_agent(tmp_path): + """The source is read off the reception stage after its turn ends, so within that turn the + conversation does not know it yet. Without allowing for that, the stage that has just + succeeded is told it has produced nothing and the handoff is refused.""" + from agent_harness.chat import Conversation + from agent_harness.sources import RepoSource + + conversation = Conversation(out=tmp_path) + conversation.stage_name = "reception" + assert conversation.next_stage() is None, "nothing pointed at yet" + + # what point_at_agent does, mid-turn + conversation._found["source"] = RepoSource(name="x", root=tmp_path) + assert conversation.next_stage() == "understand" + + +def test_the_turn_that_finds_the_agent_also_opens_the_next_stage(tmp_path, monkeypatch): + """Allowing that handoff is not enough: the stage it opens is built from the source, so the + source has to be on the conversation before the hop, not after it. Otherwise the hop raises, + the turn is lost, and the source is never taken up at all — every later message arrives back + at reception, which has no tools to do anything with it.""" + import asyncio + + from agent_harness import chat as chat_module + from agent_harness.chat import Conversation + from agent_harness.sources import RepoSource + + said: list[str] = [] + + class Stage: + spent_usd = 0.0 + + def __init__(self, name: str) -> None: + self.name = name + + async def __aenter__(self): + return self + + async def __aexit__(self, *_): + return False + + async def say(self, message, on_event=None): + said.append(f"{self.name}: {message}") + + def grant(self, *_, **__): + pass + + found: dict = {} + monkeypatch.setattr( + chat_module.reception_stage, "open_stage", lambda **_: (Stage("reception"), found) + ) + monkeypatch.setattr(chat_module.reception_stage, "opening", lambda: "which agent") + monkeypatch.setattr( + chat_module.understand_stage, + "open_stage", + lambda *_, **__: (Stage("understand"), {}), + ) + monkeypatch.setattr(chat_module.understand_stage, "opening", lambda _: "read the agent") + + conversation = Conversation(out=tmp_path, workspace=tmp_path) + + async def turn(): + await conversation.open_quietly() + # what reception's turn does when one message both names the agent and asks for the next + # thing: it points, then hands the request on. + found["source"] = RepoSource(name="x", root=tmp_path) + conversation._handoff["request"] = "read it and tell me what it can do" + await conversation.say("test the voice agent at /x, and tell me what it can do") + + asyncio.run(turn()) + + assert conversation.source is not None, "the turn that pointed never landed" + assert conversation.stage_name == "understand" + assert any("read it and tell me what it can do" in one for one in said), said + + +def test_the_build_skill_documents_every_method_a_handler_can_call(): + """A handler gets `db` and nothing else, so if the skill does not say what `db` offers the + model guesses — and the guess is sqlite's cursor API, which fails on the smoke call.""" + import inspect + + from agent_harness.world.runtime import Db + + skill = load_skill("build-environment") + methods = [ + name for name, _ in inspect.getmembers(Db, inspect.isfunction) + if not name.startswith("_") + ] + assert methods, "Db should have methods to document" + for name in methods: + assert f"db.{name}(" in skill, f"the build skill never shows db.{name}()" + # and it warns off the API the model actually reaches for by default + assert "fetchone" in skill + + +def test_a_crashed_handler_is_told_what_a_handler_actually_has(tmp_path): + """An error naming the failure without naming the API produces the same wrong guess again. + Three identical attempts at one handler is what that cost on a real run.""" + import asyncio + + from mcp.types import CallToolRequest, CallToolRequestParams + + from agent_harness.world import tools as world_tools + + root, contract = _saved_world(tmp_path) + server, _world = world_tools.world_tools(contract, root) + instance = server.get("instance") if isinstance(server, dict) else server + + async def define(source): + for key, handler in instance.request_handlers.items(): + if getattr(key, "__name__", "") == "CallToolRequest": + answer = await handler( + CallToolRequest( + method="tools/call", + params=CallToolRequestParams( + name="define_handler", + arguments={"tool_name": "add", "source": source}, + ), + ) + ) + return answer.root.content[0].text + + # the mistake a model actually makes: sqlite's cursor API + said = asyncio.run(define( + "def handle(args, db):\n" + " return db.execute('SELECT 1').fetchone()\n" + )) + assert "crashed on its smoke call" in said + assert "db.query(" in said and "db.one(" in said and "db.execute(" in said + assert "fetchone" in said + + +# --- the store layer: engines, resets, and what a scenario lands on ---------------------- + + +def test_a_reset_that_forgets_its_counters_is_caught_without_naming_one(tmp_path): + """Rows going back is the easy half, and most wrong resets manage it. + + What they miss is the counter behind the rows, so the next scenario's first insert gets an id + continuing from the last one, and a check naming a specific id then fails for a reason that + has nothing to do with the agent. Rather than ask what a counter is called on this engine, + the same change is run twice from the same starting point and the results compared. + """ + from agent_harness.world.stores import resolve + from agent_harness.world.stores.prove import prove_store + + def stood_up(): + store = resolve("sqlite") + store.apply( + "CREATE TABLE orders (id INTEGER PRIMARY KEY AUTOINCREMENT, who TEXT);" + "INSERT INTO orders (who) VALUES ('ana'), ('bo');" + ) + return store + + insert = "INSERT INTO orders (who) VALUES ('new')" + assert not [one for one in prove_store(stood_up(), insert).results if not one.passed] + + # And with the counter deliberately left where it was, which is the bug itself. + forgetful = stood_up() + forgetful._reinstate = lambda counters: None + failed = [one for one in prove_store(forgetful, insert).results if not one.passed] + assert [one.name for one in failed] == ["ids do not drift"] + assert "'id': 3" in failed[0].detail and "'id': 4" in failed[0].detail + + +def test_an_engine_nobody_taught_the_harness_is_refused_by_name(): + """Handing a ClickHouse agent a Postgres would produce a green suite about SQL it never runs.""" + from agent_harness.world.stores import StoreError, resolve, supported + + with pytest.raises(StoreError) as refused: + resolve("clickhouse") + assert "clickhouse" in str(refused.value) + for engine in supported(): + assert engine in str(refused.value) + + +def test_a_written_store_has_to_say_how_a_scenario_changes_it(): + """Reading an engine is not enough. Without add, amend and remove a suite has one world. + + A store that can be stood up and read but not changed lets every scenario run against the + same base, and the per-scenario setup silently does nothing rather than failing. + """ + from agent_harness.world.stores import StoreError + from agent_harness.world.stores.written import register_written + + readable = ( + "def connect(dsn): return None\n" + "def apply(db, script): pass\n" + "def state(db): return {}\n" + "def freeze(db): return {}, {}\n" + "def restore(db, rows, counters): pass\n" + ) + with pytest.raises(StoreError) as refused: + register_written(engine="ledgerdb", image="x:1", container_port=1, code=readable) + said = str(refused.value) + assert "add" in said and "amend" in said and "remove" in said + + +def test_an_agent_that_holds_its_own_data_is_reached_by_its_own_loader(): + """Not by reading its files and rebuilding the structure, which would be a second + implementation of the one thing this path exists to stop reimplementing.""" + from agent_harness.world.stores import resolve + + called = [] + + def load_data(): + called.append(True) + return {"orders": {"o1": {"status": "pending"}}, "notes": ["first"]} + + store = resolve("in_process", loader=load_data) + store.start() + assert called, "the agent's own loader is what fills the store" + + # A keyed group reads back as records carrying the key, because that is the id a check names. + assert store.records("orders") == [{"_id": "o1", "status": "pending"}] + assert store.records("notes") == [{"value": "first"}] + + # What a scenario's setup lands on, in both shapes. + kept = store.freeze() + store.amend("orders", "o1", {"status": "cancelled"}) + store.add("orders", {"_id": "o2", "status": "pending"}) + assert store.state()["orders"] == [ + {"_id": "o1", "status": "cancelled"}, + {"_id": "o2", "status": "pending"}, + ] + + # And going back has to reproduce the agent's own structure, not merely the records. + store.restore(kept) + assert store.data == {"orders": {"o1": {"status": "pending"}}, "notes": ["first"]} + + +def test_emptying_a_store_is_something_restore_can_reproduce(): + """The check gate empties the world and insists every check notices, so a restore that cannot + represent an empty store would make that gate impossible to run.""" + from agent_harness.world.stores import Snapshot, resolve + + store = resolve("in_process", loader=lambda: {"orders": {"o1": {"status": "pending"}}}) + store.start() + store.restore(Snapshot()) + assert store.state() == {"orders": []} + # The group itself survives, because the agent's own code indexes into it. + assert store.data == {"orders": {}} + + +def test_saving_a_world_that_already_lives_in_the_saved_file_does_not_hang(tmp_path): + """An agent that connects by URI needs the world to be a real file, and the obvious file to + give it is the one the world is saved to. + + SQLite retries a locked backup destination rather than refusing, so backing a database up onto + its own file does not fail, it hangs, with no error and no timeout. The build then stops dead + somewhere nobody is looking. + """ + import threading + + from agent_harness.world.runtime import GeneratedWorld + from agent_harness.world.snapshot import restore, save + + world = GeneratedWorld(tmp_path / "world.sqlite") + world.store.apply("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT); INSERT INTO t (v) VALUES ('a');") + + done = threading.Event() + + def keep(): + save(world, tmp_path, sequences=[]) + done.set() + + worker = threading.Thread(target=keep, daemon=True) + worker.start() + assert done.wait(20), "saving a world onto its own file hung" + assert len(restore(tmp_path).state()["t"]) == 1 + + +def test_a_world_takes_the_agents_own_store_rather_than_retyping_it(tmp_path): + """Seeding by hand means retyping somebody's data through a model, and what comes out is + smaller and tidier than what went in: fewer rows, the awkward ones dropped, the accented names + spelled the easy way. The agent's queries were written against the real thing.""" + import sqlite3 + + from agent_harness.world.runtime import GeneratedWorld + + theirs = tmp_path / "theirs.db" + origin = sqlite3.connect(theirs) + origin.executescript( + "CREATE TABLE Customer (CustomerId INTEGER PRIMARY KEY, Name TEXT, Company TEXT);" + "INSERT INTO Customer (Name, Company) VALUES ('Luis Goncalves', 'Embraer');" + "INSERT INTO Customer (Name, Company) VALUES ('Leonie Kohler', NULL);" + ) + origin.commit() + origin.close() + + world = GeneratedWorld(":memory:") + world.store.take(theirs) + + held = world.state()["Customer"] + assert len(held) == 2 + # Their schema, not one inferred from the rows: the nullable column survives as null. + assert held[1]["Company"] is None + # And taking it is read-only on their side, so testing an agent never edits its data. + assert theirs.stat().st_size > 0 + + +def test_a_missing_store_names_the_root_and_what_is_under_it(tmp_path): + """A message that says a path was wrong without saying what the right ones are turns one call + into a search, over a filesystem this stage deliberately cannot list. That is how the same + wrong guess gets made three times.""" + from agent_harness.world.tools import _stores_here + + (tmp_path / "nested").mkdir() + (tmp_path / "nested" / "chinook.db").write_bytes(b"x" * 4096) + (tmp_path / "agent.py").write_text("print('hi')", encoding="utf-8") + + said = _stores_here(str(tmp_path)) + assert str(tmp_path) in said, "the root itself has to be named" + assert "nested/chinook.db" in said + assert "4 KB" in said + # A python file is not a store, and listing it would bury the one that is. + assert "agent.py" not in said + + # And with no root at all, say that rather than reporting an empty directory. + assert "not told where" in _stores_here("") + + +def test_a_tool_that_cannot_be_reached_has_a_way_out_that_is_recorded(tmp_path): + """Without one there is no legitimate exit at all: define_handler refuses because the tool has + an implementation, adopt_tool fails because that implementation needs something this + environment does not have, and the only moves left are to give up or to lie.""" + from agent_harness.amend import unreachable + from agent_harness.contract import AgentContract + + contract = AgentContract( + agent="x", + tools=[{"name": "look", "args": ["q"]}], + real_use_cases=["a plain sentence"], + tool_entrypoints=[ + {"tool": "look", "mode": "construct", "module": "vendor.tools", "callable": "Look._run"} + ], + ) + assert contract.adoptable("look") + + # Not without a reason: this is the only record that the tool was a stand-in. + held, said = unreachable(contract, tmp_path, tool_name="look", why=" ") + assert not held and "say why" in said + + held, said = unreachable( + contract, tmp_path, tool_name="look", why="built by a framework that needs a live client" + ) + assert held + # The refusal is lifted, so a handler can be written. + assert not contract.adoptable("look") + # And the reason survives, on the contract, where a reader will find it. + assert any("could not be reached" in one for one in contract.amendments) + assert "live client" in contract.entry_for("look").notes + + kept = json.loads((tmp_path / "contract.json").read_text(encoding="utf-8")) + assert kept["tool_entrypoints"][0]["mode"] == "generate" + + # And a tool that never had an implementation was never blocked, so there is nothing to record. + again, said = unreachable(contract, tmp_path, tool_name="look", why="same reason") + assert not again and "nothing is blocking" in said + + +def test_a_refusal_convention_written_with_escaped_quotes_still_matches(): + """A convention written for people gets quoted the way people quote, and a model writing JSON + escapes those quotes. Left in, the backslash lands inside the marker, so "Error:" is looked for + as 'Error:\\' and matches nothing. Every refusal is then recorded as a success, silently.""" + from agent_harness.world.runtime import GeneratedWorld + + world = GeneratedWorld(":memory:") + world.refusal_signature = ( + 'sql_db_query and sql_db_schema return plain error strings beginning with \\"Error:\\" ' + '(e.g. \\"Error: (sqlite3.OperationalError) ...\\") on failure rather than raising' + ) + assert world._markers(world.refusal_signature)[0] == "Error:" + assert world._refused_by_value("Error: DML statements are not permitted.") + assert not world._refused_by_value("[('AC/DC',), ('Accept',)]") + + # And the plain unescaped form, which is what a human writing the contract would put. + world.refusal_signature = 'strings starting with "Error: "' + assert world._refused_by_value("Error: no such order") + + +def test_emptying_a_world_survives_foreign_keys(tmp_path): + """A real schema has references. Deleting table by table fails on the referenced ones, and a + caller that swallows those failures believes it emptied a store still holding most of its data. + + The gate then reports every check as verifying nothing, because they are all still reading real + rows, and somebody rewrites checks that were correct. + """ + from agent_harness.world.mutate import _empty, left + from agent_harness.world.runtime import GeneratedWorld + + world = GeneratedWorld(":memory:") + world.store.apply( + "CREATE TABLE Artist (ArtistId INTEGER PRIMARY KEY, Name TEXT);" + "CREATE TABLE Album (AlbumId INTEGER PRIMARY KEY, Title TEXT, ArtistId INTEGER," + " FOREIGN KEY (ArtistId) REFERENCES Artist (ArtistId));" + "INSERT INTO Artist (ArtistId, Name) VALUES (1, 'AC/DC');" + "INSERT INTO Album (AlbumId, Title, ArtistId) VALUES (1, 'Let There Be Rock', 1);" + ) + assert sum(len(rows) for rows in world.state().values()) == 2 + + _empty(world) + assert left(world) == {}, "a referenced table has to be emptied too" + + +def test_a_mutation_that_does_not_land_accuses_nobody(): + """The gate accuses a check of verifying nothing when it stays green through damage. That is + only fair if the damage happened.""" + from agent_harness.world.mutate import EMPTIED, SILENCED, UNDAMAGED, blind, unnoticed + from agent_harness.world.runtime import GeneratedWorld + + class Stubborn(GeneratedWorld): + def __init__(self): + super().__init__(":memory:") + self.store.apply("CREATE TABLE t (v TEXT); INSERT INTO t VALUES ('kept');") + # A store that quietly refuses to empty, which is what a foreign key looked like. + self.store.clear = lambda: None + + held = type("Outcome", (), {"held": True})() + survived = unnoticed( + "anywhere", + [("reads_the_world", "def check(world):\n return None\n")], + run=lambda source, world: held, + restore=lambda _root: Stubborn(), + ) + + assert survived[EMPTIED] == [], "nothing can be concluded from damage that did not happen" + assert survived[UNDAMAGED] and "would not empty" in survived[UNDAMAGED][0] + # The check stayed green through silencing, but that alone is not blindness. + assert survived[SILENCED] == ["reads_the_world"] + assert blind(survived) == [] + + +def test_an_opening_line_in_the_agents_voice_falls_back_to_the_instruction(): + """The opening turn is the one with no conversation behind it, and a model asked to speak into + that gap sometimes takes the other part. + + Both of these are real opening lines from a run: the agent then replied that no question had + been asked, and the scenario failed for a reason that had nothing to do with the agent. + """ + from agent_harness.run.conversation import OPENING, _answered_as_the_agent + + assert _answered_as_the_agent("Sure! Let me find the database and make that update for you.") + assert _answered_as_the_agent("I'd be happy to look that up! Let me check the database.") + assert _answered_as_the_agent("What would you like to know about the database?") + + # What a person actually says, which must survive untouched. + assert not _answered_as_the_agent("How many of your customers are based in Canada?") + assert not _answered_as_the_agent( + "I want all track prices changed to 0.99 for the promotion, can you update them?" + ) + assert not _answered_as_the_agent("Which genre has the highest average listener rating?") + + # And the instruction that produces the opening says which part to play. + assert "you speak first" in OPENING + assert "not \nthe agent being contacted" in OPENING or "not the agent" in OPENING + + +def test_a_refusal_scenario_is_not_vacuous_because_its_evidence_is_what_was_said(): + """Where the right behaviour is to decline and touch nothing, every check about the world holds + with nothing done, and the explanation is the only real evidence. + + Judged on that alone, the gate rejects exactly the scenarios that test a refusal. An agent that + did nothing also said nothing, so a judged sub-goal cannot be passed by an empty run. + """ + from agent_harness.checks import Outcome + from agent_harness.catalogue import Catalogue, SubGoal + from agent_harness.prove import Proof + + catalogue = Catalogue( + sub_goals=[ + SubGoal(name="no_dml_attempted", what="nothing was written", check="def check(w,c):\n return None\n"), + SubGoal(name="refused_clearly", what="it explained the refusal", judged="needs the reply read"), + ] + ) + assert catalogue.named("no_dml_attempted").deterministic() + assert not catalogue.named("refused_clearly").deterministic() + + # The state check holds with nothing done, which on its own would read as vacuous. + proof = Proof() + proof.with_nothing = [Outcome("no_dml_attempted", True, "")] + proof.weak = ["no_dml_attempted"] + + everything_weak = len(proof.weak) == len(proof.with_nothing) + assert everything_weak + judged = [ + name + for name in ["no_dml_attempted", "refused_clearly"] + if (found := catalogue.named(name)) is not None and not found.deterministic() + ] + assert judged == ["refused_clearly"] + # Which is what spares the scenario. + assert not (everything_weak and not judged) + + +def test_a_setup_or_ready_that_says_nothing_is_not_a_complaint(): + """The convention is that a complaint is a sentence. An empty string reads as "no complaint" to + whoever wrote it, and taking it as a failure produces a rejection with no reason attached: the + author is then sent hunting for a problem that is not there.""" + from agent_harness.folder import _run + + for returning in ("''", "' '", "None", "True"): + outcome = _run(f"def ready(world):\n return {returning}\n", "s/ready.py", "ready", None) + assert outcome.ok, f"returning {returning} should read as holding" + assert outcome.said == "" + + # A real complaint survives untouched. + complained = _run( + "def ready(world):\n return 'no pending orders'\n", "s/ready.py", "ready", None + ) + assert not complained.ok and complained.said == "no pending orders" + + # And False is a failure that says nothing, so the message says that rather than being blank. + bare = _run("def ready(world):\n return False\n", "s/ready.py", "ready", None) + assert not bare.ok and "without saying what is wrong" in bare.said + + +def test_an_optional_field_may_be_null(): + """Filling a field that does not apply with null is what a model does, and it is not wrong: the + alternative is inventing a value. + + Rejecting it costs a turn, and the rejection does not say which field was at fault. "None is + not of type 'string'" is the whole message, on a tool with twenty properties. + """ + import jsonschema + + from agent_harness.tools import schema + + shape = schema({"name": str, "note": str, "size": int}, ["name"]) + + # Required stays strict. + assert shape["properties"]["name"]["type"] == "string" + assert shape["properties"]["note"]["type"] == ["string", "null"] + assert shape["properties"]["size"]["type"] == ["integer", "null"] + + jsonschema.validate({"name": "x", "note": None, "size": None}, shape) + with pytest.raises(jsonschema.ValidationError): + jsonschema.validate({"name": None}, shape) + + # A property given as a full fragment is left exactly as written. + spelled = schema({"size": {"type": "string", "enum": ["S", "M"]}}, []) + assert spelled["properties"]["size"] == {"type": "string", "enum": ["S", "M"]} + + +def test_an_agent_with_no_store_still_gets_a_world_that_holds_things(): + """An agent whose state lives in services and files has no store to declare collections in, so + every collection the world needs is one the harness invents. + + Refusing the first record leaves that agent with a world that cannot hold anything at all, and + there is no other way for the build to put its stand-in data somewhere. + """ + from agent_harness.world.runtime import GeneratedWorld + + world = GeneratedWorld(":memory:", kind="in_process") + assert world.state() == {} + + world.put("reports", {"title": "first"}) + world.put("reports", {"title": "second"}) + assert len(world.state()["reports"]) == 2 + + # A keyed collection stays keyed, so a scenario can name one record. + world.put("sources", {"url": "a"}, key="s1") + assert world.change("sources", "s1", {"url": "b"}) == 1 + assert world.state()["sources"] == [{"_id": "s1", "url": "b"}] + + assert world.drop("reports") == 2 + assert world.state()["reports"] == [] + + +def test_a_handler_can_read_a_world_that_has_no_query_language(): + """An agent whose state lives in services and files gets a world whose collections the harness + invented, and there is no dialect to write a SELECT in. + + A handler that could only issue SQL would be unable to read the world it was given at all, + which is where the build stalls: seed works, then nothing can look at what was seeded. + """ + from agent_harness.world.runtime import GeneratedWorld + + world = GeneratedWorld(":memory:", kind="in_process") + world.put("search_results", {"query": "solar", "title": "Solar in 2026", "rank": 1}) + world.put("search_results", {"query": "wind", "title": "Wind at sea", "rank": 1}) + + world.handlers = { + "search": ( + "def handle(args, db):\n" + " found = db.find('search_results', query=args['query'])\n" + " if not found:\n" + " raise ToolError('no results')\n" + " return [one['title'] for one in found]\n" + ) + } + assert world.call("search", {"query": "solar"}).result == ["Solar in 2026"] + # And a handler can still say no, which is the behaviour worth testing. + assert world.call("search", {"query": "nothing"}).refused + + # The collection names are reachable too, without knowing what kind of store this is. + assert world.call( + "search", {"query": "wind"} + ).ok + + +def test_a_world_with_no_database_can_still_be_reverted(): + """Every probe and every smoke call takes a checkpoint first, so a world that cannot be + checkpointed cannot be built at all: define_handler fails before the handler body even runs, + and the error names the store rather than anything the author wrote.""" + from agent_harness.world.runtime import GeneratedWorld + + world = GeneratedWorld(":memory:", kind="in_process") + world.put("reports", {"title": "first"}) + + held = world.checkpoint() + world.put("reports", {"title": "second"}) + assert len(world.state()["reports"]) == 2 + + world.revert(held) + assert [one["title"] for one in world.state()["reports"]] == ["first"] + + # And defining a handler, which is what actually stalled: it checkpoints around the smoke call. + world.handlers = {"look": "def handle(args, db):\n return len(db.records('reports'))\n"} + assert world.call("look", {}).result == 1 + + +def test_a_world_with_no_database_still_counts_as_built(tmp_path): + """Whether a stage is done is keyed on the manifest, not on a database file. + + Keyed on the database, an agent whose state lives in services and files stays "not built" + forever: the world saves, scores 1.00, and the conversation still cannot leave the build stage + because the file it is looking for was never going to exist. + """ + from agent_harness.chat import Conversation + from agent_harness.world.runtime import GeneratedWorld + from agent_harness.world.snapshot import save + + world = GeneratedWorld(":memory:", kind="in_process") + world.put("reports", {"title": "first"}) + save(world, tmp_path, sequences=[]) + + assert not (tmp_path / "world.sqlite").exists(), "this world has no database, by construction" + assert Conversation(out=tmp_path).world_built + + # And a world that does have one is still built, which is the case that already worked. + other = tmp_path / "sql" + save(GeneratedWorld(":memory:"), other, sequences=[]) + assert Conversation(out=other).world_built + + +def test_a_storeless_world_comes_back_on_the_right_side_of_the_seam(tmp_path): + """The store and the agent's own state are two different things, and both are written beside + the world. Sharing one filename means whichever is written second wins. + + The symptom is quiet: the world reads correctly, because state() merges both sides, but the + store is empty. The mutation gate then empties a store that was never holding anything and + reports every check as verifying nothing. + """ + from agent_harness.world.mutate import _empty, left + from agent_harness.world.runtime import GeneratedWorld + from agent_harness.world.snapshot import restore, save + + world = GeneratedWorld(":memory:", kind="in_process") + world.put("reports", {"title": "first"}) + world.put("reports", {"title": "second"}) + save(world, tmp_path, sequences=[]) + + again = restore(tmp_path) + assert again.store.state()["reports"], "the records belong to the store" + assert again.state_object is None, "and not to the agent's own state, which it never had" + assert len(again.state()["reports"]) == 2 + + # Which is what lets the gate actually break this world. + _empty(again) + assert left(again) == {} + + +def test_dropping_a_scenario_removes_it_from_disk(tmp_path): + """The folders are the truth and they are what gets read back. Writing the survivors without + taking the others away means a dropped scenario returns on the next load, still failing, and + dropping it appears to do nothing at all.""" + from agent_harness.catalogue import Catalogue, SubGoal + from agent_harness.scenario import Scenario + from agent_harness.scenario_tools import load_scenarios, write_scenarios + + catalogue = Catalogue( + sub_goals=[SubGoal(name="held", what="it held", check="def check(w,c):\n return None\n")] + ) + made = [ + Scenario(name="keeper", instruction="a thing happens", sub_goals=["held"]), + Scenario(name="goner", instruction="another thing", sub_goals=["held"]), + ] + write_scenarios(made, tmp_path, catalogue) + assert sorted(one.name for one in load_scenarios(tmp_path)) == ["goner", "keeper"] + + write_scenarios([made[0]], tmp_path, catalogue) + assert [one.name for one in load_scenarios(tmp_path)] == ["keeper"] + assert not (tmp_path / "scenarios" / "goner").exists() + + +def test_a_target_refuses_a_model_it_cannot_drive(): + """Handed a model it cannot speak to, this target does not fail: it produces a session that + answers nothing, which arrives as a scenario with no turns, no calls and every check red. + + That reads exactly like an agent that ignored the person, and a whole suite is then wrong in a + way nobody would think to question. It cost a full run to find. + """ + from agent_harness.run.targets import _drivable + + # What it runs on. + _drivable(None) + _drivable("claude-sonnet-4-6") + _drivable("anthropic/claude-opus-4-7") + + with pytest.raises(RuntimeError) as refused: + _drivable("vertex_ai/gemini-2.5-flash") + assert "cannot run" in str(refused.value) + # And it says where to go instead, rather than only saying no. + assert "endpoint adapters" in str(refused.value) + + +def test_a_run_is_a_folder_that_can_be_read_back(tmp_path): + """A session accumulates runs. One simulation over a suite is one run, kept whole, so runs can + be compared instead of the next one overwriting the last.""" + from agent_harness.run.grade import Result + from agent_harness.run.simulation import _write_case, every_run, read_run, run_root + + root = run_root(tmp_path, "run-1") + folder = root / "a-scenario" + folder.mkdir(parents=True) + kept = Result(scenario="a-scenario", transcript="user: hi\nagent: hello") + kept.calls_detail = [{"name": "look", "arguments": {"q": "x"}, "ok": True, "at": 1.5}] + _write_case(folder, kept) + (root / "run.json").write_text( + json.dumps({"run_id": "run-1", "agent": "x", "scenarios": 1, "passed": 1}), + encoding="utf-8", + ) + + listed = every_run(tmp_path) + assert [one["run_id"] for one in listed] == ["run-1"] + + whole = read_run(tmp_path, "run-1") + assert whole["scenarios"][0]["scenario"] == "a-scenario" + assert "hello" in whole["scenarios"][0]["transcript"] + # Down to a single call, which is what the harness is asked about when a run is questioned. + assert whole["scenarios"][0]["calls_detail"][0]["name"] == "look" + + +def test_the_closing_line_is_kept(): + """The sentinel used to be the whole reply, and breaking on it threw the words away. + + Every conversation then ended on the agent's turn with nothing after it: a transcript that + reads as cut off rather than finished, and no way to tell a person who left satisfied from + one who was still waiting for an answer. + """ + from agent_harness.run.conversation import DONE, STUCK, customer_prompt + + said = "Thanks, that's exactly what I needed.\n[DONE]" + closing = said.replace(DONE, "").replace(STUCK, "").strip() + assert closing == "Thanks, that's exactly what I needed." + + # And a bare sentinel still ends it, without recording an empty turn. + assert not "[DONE]".replace(DONE, "").replace(STUCK, "").strip() + + # The person is asked for that line, and told not to leave while the agent is waiting. + from agent_harness.contract import AgentContract + from agent_harness.scenario import Scenario + + asked = customer_prompt( + Scenario(name="s", instruction="you want a thing", sub_goals=[]), + AgentContract(agent="x", tools=[{"name": "t", "args": ["a"]}], real_use_cases=["a sentence"]), + ) + assert "the one line you would actually say to end it" in asked + assert "not the end of the conversation" in asked + + +def test_a_judged_sub_goal_becomes_a_named_platform_eval(): + """The sentence a sub-goal already is happens to be exactly what a custom eval wants, so it + becomes one: created once, versioned, visible in the product rather than only in a run + folder, and reusable against production traffic later without being rewritten.""" + from agent_harness.run import platform_evals + + name = platform_evals.eval_name("text-to-sql", "refused_dml_clearly") + assert name == "text-to-sql-refused_dml_clearly" + # The agent is in the name because uniqueness is per organisation: a bare sub-goal name + # would collide across every agent anybody tests. + assert platform_evals.eval_name("other-agent", "refused_dml_clearly") != name + # And the platform only accepts a restricted alphabet. + assert platform_evals.eval_name("Drive Thru!", "no DML") == "drive-thru--no-dml" + + written = platform_evals.instructions_for( + "the agent explained why it could not", "shop", ["never write to the database"] + ) + # One variable, declared by being written: the platform extracts it from the instructions. + assert "{{conversation}}" in written + assert "never write to the database" in written + + +def test_a_verdict_is_read_however_it_arrives(): + """A pass comes back as a word or a number depending on the eval's output type, and reading + only one shape would silently fail every eval configured the other way.""" + from agent_harness.run.platform_evals import _passed + + assert _passed("Pass") and _passed("pass") and _passed("PASSED") + assert _passed(True) and _passed(1.0) and _passed(0.5) + assert not _passed("Fail") and not _passed(False) and not _passed(0.2) + assert not _passed(None) + + +def test_the_platform_is_used_only_when_it_is_configured(): + """Without keys the harness judges here instead. A suite that cannot run without a platform + account is a worse tool than one that degrades.""" + import os + + from agent_harness.run import platform_evals + + kept = {name: os.environ.pop(name, None) for name in platform_evals.KEYS} + try: + assert not platform_evals.configured() + os.environ["FI_API_KEY"] = "x" + assert not platform_evals.configured(), "both keys are needed, not one" + os.environ["FI_SECRET_KEY"] = "y" + assert platform_evals.configured() + finally: + for name, value in kept.items(): + os.environ.pop(name, None) + if value is not None: + os.environ[name] = value + + +def test_voice_suite_evals_use_the_documented_platform_inputs(monkeypatch): + from agent_harness.catalogue import default_suite_evals + from agent_harness.contract import AgentContract + from agent_harness.run.conversation import Exchange, Transcript + from agent_harness.run.grade import judge_suite_evals + from agent_harness.run import platform_evals + from agent_harness.scenario import Scenario + + calls = [] + + def judge_builtin(name, inputs): + calls.append((name, inputs)) + output = "Pass" if name == "customer_agent_task_completion" else {"choice": "4"} + return {"output": output, "why": "verified", "model": "turing_flash"} + + monkeypatch.setattr(platform_evals, "configured", lambda: True) + monkeypatch.setattr(platform_evals, "judge_builtin", judge_builtin) + contract = AgentContract( + agent="voice-agent", + modality="voice", + system_prompt_excerpt="Resolve support requests accurately.", + tools=[{"name": "lookup", "args": ["order_id"]}], + real_use_cases=["support"], + ) + transcript = Transcript( + exchanges=[Exchange("customer", "Where is my order?"), Exchange("agent", "It arrives tomorrow.")] + ) + verdicts = judge_suite_evals( + default_suite_evals(), + Scenario(name="order-status", instruction="check an order", sub_goals=[]), + transcript, + contract, + ) + + assert calls == [ + ( + "customer_agent_task_completion", + { + "agent_prompt": "Resolve support requests accurately.", + "conversation": "customer: Where is my order?\nagent: It arrives tomorrow.", + }, + ), + ( + "customer_agent_conversation_quality", + {"conversation": "customer: Where is my order?\nagent: It arrives tomorrow."}, + ), + ] + assert all(verdict.holds for verdict in verdicts) + + +def test_suite_evals_do_not_run_for_non_voice_agents(monkeypatch): + from agent_harness.catalogue import default_suite_evals + from agent_harness.contract import AgentContract + from agent_harness.run.conversation import Transcript + from agent_harness.run.grade import judge_suite_evals + from agent_harness.run import platform_evals + from agent_harness.scenario import Scenario + + monkeypatch.setattr(platform_evals, "configured", lambda: True) + monkeypatch.setattr(platform_evals, "judge_builtin", lambda *_args: pytest.fail("should not run")) + contract = AgentContract( + agent="chat-agent", + modality="chat", + tools=[{"name": "lookup", "args": ["order_id"]}], + real_use_cases=["support"], + ) + assert judge_suite_evals( + default_suite_evals(), + Scenario(name="status", instruction="check", sub_goals=[]), + Transcript(), + contract, + ) == [] diff --git a/harness/ui/README.md b/harness/ui/README.md new file mode 100644 index 0000000..4575592 --- /dev/null +++ b/harness/ui/README.md @@ -0,0 +1,143 @@ +# The harness, as a chat + +A web page you talk to. Same harness, same stages, same artifacts as the CLI. This is a second +renderer over the event stream the stages already emit, not a second implementation. + +There is no separate front end to build or start. The page is one static file this server hands +out on `/`, and it talks to the same server's JSON endpoints. No node, no npm, no build step. + +## Setting it up + +```bash +git clone https://github.com/future-agi/agent-learning-kit +cd agent-learning-kit +cd harness + +uv sync +``` + +The harness installs the editable parent `agent-learning-kit` package and the UI's `fastapi` and +`uvicorn` dependencies from its own manifest. + +You also need the `claude` command on your PATH (`npm install -g @anthropic-ai/claude-code`). +The harness talks to the model through the Claude Agent SDK, which runs that binary underneath; +without it every stage fails immediately. + +Credentials go through **Vertex AI**, not a plain Anthropic key: `config.provider_env` sets +`CLAUDE_CODE_USE_VERTEX=1`, so `ANTHROPIC_API_KEY` on its own will not work. Copy the template +and fill in your service account: + +```bash +cp ../oss/simulation-acceptance/.env.example .env.acceptance +# GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/your-service-account.json +# GOOGLE_CLOUD_PROJECT=your-gcp-project-id +``` + +`.env.acceptance` is git-ignored and points at a private key. Never commit it or paste its +contents anywhere. + +Then load it and pick the model, in each new terminal: + +```bash +set -a; . ./.env.acceptance; set +a +export CLOUD_ML_REGION=global +export ALK_HARNESS_MODEL=claude-sonnet-4-6 +``` + +It prints the model and which credentials it found before starting, so a run never begins on +something you did not intend. **Use Sonnet or better.** Haiku has misread an agent's modality, +and modality decides how every later test is run. + +## Running it + +```bash +uv run python ui/server.py +``` + +Open **http://localhost:8777**, press **+ new**, and say what you want tested: + +``` +i want to test my voice ordering agent. the code is at /absolute/path/to/the/agent +``` + +One message is enough to begin. Reception takes the path out of the sentence and hands straight +over to reading the agent. From there it is a conversation: + +``` +now build the environment for it +write me 5 scenarios: a plain order, one for something you do not have, one where the +customer changes their mind, one that pushes against a rule, and one with quantity +``` + +Questions in between are answered without spending a stage, so "can it handle quantity?" is a +fair thing to ask mid-flight. + +To stop the server, Ctrl-C. Checking whether it is still up with `lsof -ti:8777` will mislead +you: that matches a browser's leftover sockets. Use `lsof -nP -iTCP:8777 -sTCP:LISTEN`. + +**Restart the server after changing anything under `src/agent_harness/`.** A long-lived process +does not pick up code or skills on its own. + +## What you can do in it + +- **Start, reopen or delete a conversation** from the picker. Everything about one conversation + lives in its own folder under `artifacts/sessions//`, so reopening it restores the chat and + every artifact. A blank slate is `rm -rf artifacts/sessions/* artifacts/.open-session`. +- **Talk.** "build the world", "write 5 hard scenarios", "make that one harder", "add a mango + smoothie to the menu". Each reply shows the work underneath it: which tool ran, what it + answered, what it refused. +- **Move between stages** by clicking the roadmap. Stages are not a wizard: going back to correct + a contract after the world is built is the ordinary case. A stage whose input does not exist + yet says why it cannot be opened. +- **Read the four tabs.** Contract, Environment, Scenarios and Runs are what is on disk. Each + scenario shows its instruction, what it changes, its reference solution, its checks, and three + gate lights; its files open inline. +- **Run the scenarios.** The conversation between the simulated customer and the agent streams + into the chat as it happens, then a verdict lands with the checks. + +## What to expect while it works + +The build stage is the long one: roughly 30 turns and about ten minutes for a five-tool agent. +**The Environment tab stays empty until it finishes**, because the world is held in memory until +`save_world` writes it. The chat is where the progress is: schema, seeds, then one line per +handler as each is defined and smoke-called. + +## The two files + +| File | What it is | +|---|---| +| `server.py` | FastAPI. Holds one `Conversation` open, streams its events as server-sent events, and serves the artifacts as JSON. | +| `static/index.html` | The whole interface: markup, styling and rendering in one file. | + +To restyle it, edit the ` + + +
+ +
+
+ + + +
+ +
+
+ +
+
+
+
+
+
+ + + +
+
+
+
+ +
+
+
+
+
+
+ + + + diff --git a/harness/uv.lock b/harness/uv.lock new file mode 100644 index 0000000..7ebda28 --- /dev/null +++ b/harness/uv.lock @@ -0,0 +1,5022 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11'", +] + +[[package]] +name = "absl-py" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/4f/d79676ab82f2e42fc3611618139f13a9c4c31d0cff4b486982047679a802/absl_py-2.5.0.tar.gz", hash = "sha256:0c996f25c0490700fadabe6351630f6111534fa0ae252cc6d2014ea3b141135f", size = 118119, upload-time = "2026-07-03T10:57:48.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/0a/a10b45aab35b175aded078a462dc8d0c698f5b13946e7cb0869097b78bb6/absl_py-2.5.0-py3-none-any.whl", hash = "sha256:0f17b89f2a4eaaedc4f28c622998aa690564b3012a396a4ffad0821007fe03ba", size = 137410, upload-time = "2026-07-03T10:57:46.735Z" }, +] + +[[package]] +name = "agent-harness" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "agent-learning-kit", extra = ["livekit"] }, + { name = "fastapi" }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-learning-kit", extras = ["livekit"], editable = "../" }, + { name = "fastapi", specifier = ">=0.115,<1" }, + { name = "uvicorn", specifier = ">=0.30,<1" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.3" }] + +[[package]] +name = "agent-learning-kit" +version = "0.1.0" +source = { editable = "../" } +dependencies = [ + { name = "claude-agent-sdk" }, + { name = "fi-instrumentation-otel" }, + { name = "gepa" }, + { name = "httpx" }, + { name = "jsonschema" }, + { name = "levenshtein" }, + { name = "litellm" }, + { 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 = "openai" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-sdk" }, + { name = "optuna" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-futures" }, + { name = "rich" }, + { name = "rouge-score" }, + { name = "typer" }, +] + +[package.optional-dependencies] +livekit = [ + { name = "aiohttp" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "livekit-agents", extra = ["cartesia", "deepgram", "google", "openai", "silero"] }, + { name = "livekit-plugins-elevenlabs" }, +] + +[package.metadata] +requires-dist = [ + { name = "a2a-sdk", extras = ["http-server"], marker = "extra == 'a2a'", specifier = ">=1.1.0" }, + { name = "aiohttp", marker = "extra == 'all'", specifier = ">=3.10" }, + { name = "aiohttp", marker = "extra == 'livekit'", specifier = ">=3.10" }, + { name = "aiohttp", marker = "extra == 'trinity'", specifier = ">=3.10" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13' and extra == 'all'", specifier = ">=0.2.1" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13' and extra == 'livekit'", specifier = ">=0.2.1" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13' and extra == 'trinity'", specifier = ">=0.2.1" }, + { name = "chromadb", marker = "extra == 'all'", specifier = ">=0.4.0" }, + { name = "chromadb", marker = "extra == 'feedback'", specifier = ">=0.4.0" }, + { name = "claude-agent-sdk", specifier = ">=0.2.139" }, + { name = "fastapi", marker = "extra == 'harness-ui'", specifier = ">=0.115,<1" }, + { name = "fi-instrumentation-otel", specifier = ">=0.1.16" }, + { name = "gepa", specifier = ">=0.0.17" }, + { name = "httpx", specifier = ">=0.24.0" }, + { name = "ipykernel", marker = "extra == 'notebook'", specifier = ">=6" }, + { name = "jsonschema", specifier = ">=4.25.1,<5" }, + { name = "langchain-core", marker = "extra == 'langchain'", specifier = ">=1.4.6,<2" }, + { name = "langgraph", marker = "extra == 'langchain'", specifier = ">=1.2.4,<2" }, + { name = "langgraph-checkpoint-sqlite", marker = "extra == 'langchain'", specifier = ">=3.1.0" }, + { name = "levenshtein", specifier = ">=0.25.0" }, + { name = "litellm", specifier = ">=1.80.0,<2" }, + { name = "livekit-agents", extras = ["cartesia", "deepgram", "openai", "silero", "google"], marker = "extra == 'all'", specifier = ">=1.2" }, + { name = "livekit-agents", extras = ["cartesia", "deepgram", "openai", "silero", "google"], marker = "extra == 'livekit'", specifier = ">=1.2" }, + { name = "livekit-agents", extras = ["cartesia", "deepgram", "openai", "silero", "google"], marker = "extra == 'trinity'", specifier = ">=1.2" }, + { name = "livekit-plugins-elevenlabs", marker = "extra == 'all'", specifier = ">=1.2" }, + { name = "livekit-plugins-elevenlabs", marker = "extra == 'livekit'", specifier = ">=1.2" }, + { name = "livekit-plugins-elevenlabs", marker = "extra == 'trinity'", specifier = ">=1.2" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.27,<2" }, + { name = "nbformat", marker = "extra == 'notebook'", specifier = ">=5" }, + { name = "nltk", specifier = ">=3.9.0" }, + { name = "numpy", specifier = ">=1.26.4" }, + { name = "openai", specifier = ">=1.109.1,<3" }, + { name = "opentelemetry-api", specifier = ">=1.39.1,<2" }, + { name = "opentelemetry-exporter-otlp", specifier = ">=1.39.1,<2" }, + { name = "opentelemetry-sdk", specifier = ">=1.39.1,<2" }, + { name = "optuna", specifier = ">=3.6.1" }, + { name = "pandas", specifier = ">=2.0.0" }, + { name = "pipecat-ai", marker = "extra == 'pipecat'", specifier = ">=0.0.108" }, + { name = "pydantic", specifier = ">=2.0,<3" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "requests", specifier = ">=2.32.5,<3" }, + { name = "requests-futures", specifier = ">=1.0.0" }, + { name = "rich", specifier = ">=13.0.0" }, + { name = "rouge-score", specifier = ">=0.1.2" }, + { name = "sentence-transformers", marker = "extra == 'all'", specifier = ">=5.2.3,<6" }, + { name = "sentence-transformers", marker = "extra == 'embeddings'", specifier = ">=5.2.3,<6" }, + { name = "torch", marker = "extra == 'all'", specifier = ">=2.10.0,<3" }, + { name = "torch", marker = "extra == 'nli'", specifier = ">=2.10.0,<3" }, + { name = "transformers", marker = "extra == 'all'", specifier = ">=5.2.0,<6" }, + { name = "transformers", marker = "extra == 'nli'", specifier = ">=5.2.0,<6" }, + { name = "typer", specifier = ">=0.9.0,<1.0.0" }, + { name = "uvicorn", marker = "extra == 'harness-ui'", specifier = ">=0.30,<1" }, +] +provides-extras = ["simulate", "evaluation", "optimize", "livekit", "langchain", "pipecat", "mcp", "a2a", "nli", "embeddings", "harness-ui", "feedback", "notebook", "trinity", "all"] + +[package.metadata.requires-dev] +dev = [ + { name = "build", specifier = ">=1.5" }, + { name = "fastapi", specifier = ">=0.115,<1" }, + { name = "hatchling", specifier = ">=1.25" }, + { name = "pytest", specifier = ">=8.3" }, + { name = "ruff", specifier = ">=0.9" }, + { name = "uvicorn", specifier = ">=0.30,<1" }, +] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[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 = "alembic" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/2b/e4153978368de59918115c9e01d3ebf58a558a7285efa7e960c383c4b59a/alembic-1.19.1.tar.gz", hash = "sha256:e0fca0518118c78acc493e31bcb5402f190057aaf6df8b5b95ce94c4789cf648", size = 2070816, upload-time = "2026-08-08T16:32:01.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/89/e62cc37b69ad357cc8ecd6e7367f5245f523d3cbb338a66197212bdf6749/alembic-1.19.1-py3-none-any.whl", hash = "sha256:b39018cb3d9413a19cbd54cf3c02ad33998641f0538eb77413a488a21c3e14be", size = 265946, upload-time = "2026-08-08T16:32:03.153Z" }, +] + +[[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 = "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 = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + +[[package]] +name = "av" +version = "17.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/e3/477fa20578c284abeda08d91b63ee9abaebc93445d8feeb989d3d444bae1/av-17.1.0.tar.gz", hash = "sha256:7f1e71ff621b66253333926f948e00faae11d855b2442133c65128bca64cdeb3", size = 4288546, upload-time = "2026-06-07T05:52:55.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/92/c9d0cea4f6f8f93f5b15a39f99d2d593f922484f22a2d98a8d482283e15b/av-17.1.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:19c84fd72af5ef81a20f18fbc6f9aedff9e1455e53a7062c1d4c95926d73da4e", size = 22622703, upload-time = "2026-06-07T05:51:40.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/57/74399770aa103ee4b5ff6da1781440c91a41901d89abb2433fe88773246e/av-17.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:19264c9bb4bee404accc7ce9ec461f2044b7f577a70234d29aafde31ed17de46", size = 18273538, upload-time = "2026-06-07T05:51:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/27c85b12e9ffa8f3f6854358b3eabcd91f3c29c7dac36843fa1376e833f4/av-17.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:22dff0ae582d10ef08c75c2150a4fd27cfc26653b54930c7c27b9f7b3aa20723", size = 34519101, upload-time = "2026-06-07T05:51:45.305Z" }, + { url = "https://files.pythonhosted.org/packages/04/a4/542d4bfd9f4aec5f3265985b9dbc6b259d45c2e668f9714e5f4e05b71e64/av-17.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:90c49bc9608377d01e82e747377505419a229464873341db18202d5dddecce5a", size = 36647600, upload-time = "2026-06-07T05:51:48.57Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/63bd5c59580f38109fa4c452b29b715a20c9a5eb3a078b3c447484593c40/av-17.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cc5a5247622cb77e24c342364eb68f88c1442ddfaab60c1f1f483359d3cc7879", size = 25786289, upload-time = "2026-06-07T05:51:51.674Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/78155cef0c9f8bc13f044130192c58bf962f2c9066982ff3593afe8d27f1/av-17.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff457ed419348e5b8e8c811d341389b052c5e4d5839da3794d019b125b9fe830", size = 35599848, upload-time = "2026-06-07T05:51:54.207Z" }, + { url = "https://files.pythonhosted.org/packages/76/cb/ae1d7a735a5ad9dc502dba864c51d605cbe932a769218352fd570254c38e/av-17.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1370b11a697eb3f2555906f8ab3519b0cfe48425d7830a3996ad42e6bffafda5", size = 26776479, upload-time = "2026-06-07T05:51:56.788Z" }, + { url = "https://files.pythonhosted.org/packages/fb/40/128429b9eb0c4a2beb122ed8d04b189515df68967987c2654a2e262a5c43/av-17.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3dcd41e53f53f9a3260751d9c3c11d34e93d70d61e506c81f13dbc1e3606e07b", size = 37763744, upload-time = "2026-06-07T05:51:59.222Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/5980e7bbeeadfd7a9db8e38e9f1140a3e0c392fccc31bd7b1e4a75cf5a96/av-17.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:3453b06075c7bb973fdb6de52563f7692ff05cbc64c0bb45f4fd6e8709131f2f", size = 28126516, upload-time = "2026-06-07T05:52:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/ec/87/8036b5c781bc3639ea04ef42d4e26da253bd4bd4311d8705b6a1c8824047/av-17.1.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ad7b4aa011093324b7118245f50ac6db244cfe9900d4072508a5245a2b0d3f41", size = 22460847, upload-time = "2026-06-07T05:52:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/6d/af/dfdf6fc7b17814b50d0aa9e7a7e37b87be91be3890f44b0d525433cd1fd1/av-17.1.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:43ebbe977f19a7f2d2bd1a4e119675a0b15e05852cf7309846b6ab922ba7ffe9", size = 18159115, upload-time = "2026-06-07T05:52:06.64Z" }, + { url = "https://files.pythonhosted.org/packages/ad/13/64f6c466471cea225b8b2f4cdc51a571f8a286984b55a08d169b932fda5d/av-17.1.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6a20658ec7d96a70e14b1196eff00b7cdd8831ac3b99868e16b8ba8b24090847", size = 33224427, upload-time = "2026-06-07T05:52:09.165Z" }, + { url = "https://files.pythonhosted.org/packages/77/43/96b35170bf2e64e00a41748c6400ff73232dc0fc62ded283679fb07c7fe0/av-17.1.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f9a65d1f48b818323fb411e80358f89d77dec340b01d27c6b2dfbb9cbf4b779f", size = 35370183, upload-time = "2026-06-07T05:52:11.959Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b3/8e8b4b6498731bfbd88e8399a756543f8088f1bd33d08eab678b5aebe728/av-17.1.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:58f7593726437cda5bd19793027e027768450b5c4a594777bf487798a33db702", size = 24459265, upload-time = "2026-06-07T05:52:14.66Z" }, + { url = "https://files.pythonhosted.org/packages/14/ac/ceb84b7553db21f1143d817245c560d9267168e1e58b1a8eeae2b62c4d04/av-17.1.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bbab058bd965309f39962e53caac8126987c68c0be094fc4f9427e5615b0218f", size = 34283709, upload-time = "2026-06-07T05:52:17.389Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/4115fd84148c9a1cf365096694be6ac882fd3cd3cdb7a2f35e71fecf1631/av-17.1.0-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9514cfda85180554c430695282faf4be3ffdf95775d8519733821244eecb58e0", size = 25397573, upload-time = "2026-06-07T05:52:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ac/92e52d5ed0e0b84d9d93e52b4338c2713d8a44082b8696e6516fdae7c4e4/av-17.1.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e1c90f85cd7431ede95b11e8e711571a896ebea433f298849c2c0f1594c8d86e", size = 36451495, upload-time = "2026-06-07T05:52:22.581Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/53a7cd34adb6a971d7e6d99663e74db286966c9db8afdca17472fdf0f98e/av-17.1.0-cp311-abi3-win_amd64.whl", hash = "sha256:5df5c1172ef1cf65a1529d612f7da7798ce2cf82c1ff7212466b538a6cc7214c", size = 28036393, upload-time = "2026-06-07T05:52:25.657Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/cd9ae0edf2206351c1251bb94b5ec58728e42c5f6ee16c03c412f3a1bb3e/av-17.1.0-cp311-abi3-win_arm64.whl", hash = "sha256:ee98534242a74da847af78624779ac5a3177dc7c69f956a4da9e6f0fdb37d7f6", size = 21174601, upload-time = "2026-06-07T05:52:28.077Z" }, + { url = "https://files.pythonhosted.org/packages/36/90/b5668cddb3c401fcf22553bc495d5b0c6d8a01d118624b26f0db1d0b8653/av-17.1.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:5327807c1219293803ef0c5d1578ff3ae1cf638c09e5998962026e1a554ec240", size = 22699499, upload-time = "2026-06-07T05:52:30.335Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7e/7be6bfddb823d045ff9fd5d4deb922ee3847605e162c3882e6c45b4c35ff/av-17.1.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:6c9b71fe5c0c5a8d303b1588d4d8ce9397d6b023f467cfef95000ba1f75507fa", size = 18366696, upload-time = "2026-06-07T05:52:32.645Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/391dcfa75c1ae1977efca44b753a11b929399b558826670c16a8808dd0e3/av-17.1.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f997e3351bdf51127c07a74e21741a2996e9230cbeb2d81c14acde761b116c9c", size = 36582649, upload-time = "2026-06-07T05:52:35.218Z" }, + { url = "https://files.pythonhosted.org/packages/fb/32/7312854868b318b9d1b1dcbd1bddb460aaaeac7d57f816e11efec3bef5b1/av-17.1.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:efe9b1397300b67b644ad220c89df4892a76f2debe70f16bae1749fa20526e63", size = 38479390, upload-time = "2026-06-07T05:52:37.968Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/af47f59b4458e81ca7d89f477698dbfb3d5a0cd8ae6c1e4441d01074af8a/av-17.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:fa64e1f1500d01c4a98e7a41dc1a9a35fb4dfe71f5de0389264ec1192200c76a", size = 27127432, upload-time = "2026-06-07T05:52:40.371Z" }, + { url = "https://files.pythonhosted.org/packages/88/85/c2e6861baf0f8c7d21c4ce811d4d424fedac915e3910d3570ce4377717dc/av-17.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ffbd78d73d2c9bf31e9a007c992faec3991428b2941a3b085b84fb82e8c32d19", size = 37406592, upload-time = "2026-06-07T05:52:43.215Z" }, + { url = "https://files.pythonhosted.org/packages/ba/40/3cc13125aea976101c0858af99ac47257c0654411aa199b5d8e81eea7002/av-17.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bff8896454b38fcb785a70e5ae0485d7021cb776303a5849393128a30b8f850b", size = 28336228, upload-time = "2026-06-07T05:52:46.134Z" }, + { url = "https://files.pythonhosted.org/packages/a2/38/c7d9c3e746209a1a695c13e3aa7d817229e84a85d0a84271f313d1befdd3/av-17.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1284addf3c0dd939887a9722dc30df2241a97471ad52c3c507e31583ae22ff02", size = 39490680, upload-time = "2026-06-07T05:52:48.887Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/9d42da561b7b8f7dabdfaebba07b52977bee58c5c7e4285ac991abcfaa72/av-17.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ec630be6321b04e317862f6082e84812bbd801e55a3c2298312e3fc8a0a4af4f", size = 28355673, upload-time = "2026-06-07T05:52:51.614Z" }, + { url = "https://files.pythonhosted.org/packages/a8/41/562a61d5a61fba3ffb273a115e249f1d8471b9515c59fcc38b4b9deda238/av-17.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b41647e42884bf543b8e8d0a1dabd4d1b006c99183eb1a2d7afc5b01f73eeff4", size = 21324700, upload-time = "2026-06-07T05:52:53.972Z" }, +] + +[[package]] +name = "av" +version = "18.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/f4/f22114d30d3435e38c6af2b4870f37b864403dca6ae7af747a289ce0a18e/av-18.1.0.tar.gz", hash = "sha256:47bfc286e1bc9de7ab4681fc2b575cd2460a66919d31ffe1bd5aa54fae531a28", size = 4451061, upload-time = "2026-08-12T22:28:18.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/d4/d7cdc8bff143c17a6d35924375ae28dd692cacde38700a7d419fde54f44a/av-18.1.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ae75d8bb6467895ed1f8572ededf7ffa49eac07f6e483222f5d7d62a41d12f04", size = 22546147, upload-time = "2026-08-12T22:27:11.851Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c9/37a619297492256b77d5ed906e7d8166c10a26ed251dccf1ae03ab19bff6/av-18.1.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:b30a4e8d934558e19602b68998a4d9ac9f250fa0dacef216f7e8e40153b13316", size = 18217603, upload-time = "2026-08-12T22:27:14.713Z" }, + { url = "https://files.pythonhosted.org/packages/d9/84/2464ffb64c08c5ce8b522c8e74594714414e3b0575267652c5c51c0574b9/av-18.1.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6fc837cc51adf80331ac850779cd53b5d4c4460b0ebe9057a02a921c6736f19d", size = 33640142, upload-time = "2026-08-12T22:27:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/204dbfc3e08eb4cdc6e6ff57be02150bc44523ebdb50182d10025792ebd9/av-18.1.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8a032e8d8ebc73dec079364b9b4a6837638a2d106e8472314e685ffbf163e700", size = 35786210, upload-time = "2026-08-12T22:27:20.984Z" }, + { url = "https://files.pythonhosted.org/packages/e1/99/b0d04ec553ff9a7e00455458dfa3a39c8a8f627b273056b4e5fe57d590de/av-18.1.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:3c8b1f8b46f99d52e2d8b0ed5d0cdadf172d24794d46e2077b16e44ed08e26ff", size = 39379798, upload-time = "2026-08-12T22:27:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/56/b1/e00d4feae59160149df6126585e726fdc6300798fd40c5dd324879e81f68/av-18.1.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab5ac081bc9eaf54109120d4e56284674fecfbe520d9aa1707c7fa911ec5f4d2", size = 34690321, upload-time = "2026-08-12T22:27:27.769Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/836fa987e3084d11a21489f11357fb24843ef3aa8faf74ddddfc603d5062/av-18.1.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:191224788d87af06c31784a395bb73f14b72f33d7f4871ace0157de2abdc6276", size = 36859932, upload-time = "2026-08-12T22:27:31.403Z" }, + { url = "https://files.pythonhosted.org/packages/33/b4/76ba21e46704f632004276b85289a1582e95f5eff760436d6149875a1881/av-18.1.0-cp311-abi3-win_amd64.whl", hash = "sha256:ea1480b7a8d5405cb5f382b344731bf125fd2c1c6fae3964f6c48595628387ff", size = 27595679, upload-time = "2026-08-12T22:27:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ad/a3135884c5753b09773176b97201ae602f67ad14206c395ff838d66bf9b0/av-18.1.0-cp311-abi3-win_arm64.whl", hash = "sha256:5509ec12aaa19fd6601de13cfa6f4cdad450da07982118510592875d970454d6", size = 20257584, upload-time = "2026-08-12T22:27:38.472Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5b/4a756265d7fb164336c8d377bca21c39cfa2c178be23cedee840a69b59c5/av-18.1.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:b36b0bae9e4c62f9487c99481ec15e4e3870fcc868522cd6d18fc2d6bfa04f01", size = 22795654, upload-time = "2026-08-12T22:27:42.016Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cc/1bc841462114a1adf4f7d87456ab78a6972e23271e71865fcd2bbd0e7360/av-18.1.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:025f84494cb23278498f03b0d8117d3e47a1cbc9c44b97eb31875cf02251e46b", size = 18435735, upload-time = "2026-08-12T22:27:45.787Z" }, + { url = "https://files.pythonhosted.org/packages/b8/20/005500ed17a2e62a5e4bb94aa3786942560ec2f55ec1895ebf174c87abef/av-18.1.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:08a9ae288299cfcbf739dba4ad0c53b9b71f45184303dd45947920d022fed695", size = 37090807, upload-time = "2026-08-12T22:27:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/11e7f6d848d3690c31ca4f8578167393e619177f1493ccc93b9400852d4e/av-18.1.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cf8a17466bef07765dbdecc9e66ed9b25d20b4e14f654fbf35345a58ac45fa0c", size = 38976836, upload-time = "2026-08-12T22:27:54.565Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/b271473b24e806062d31191e40c6d65545e9cf59f80f044eba56dcbba0f4/av-18.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d49a5c542dfdc00f43c6cdb6cc41dac1781ee206fe180b56aa7433dfa816dfae", size = 40896630, upload-time = "2026-08-12T22:27:59.118Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9f/2ab7fa292a947ad3466ed8e655eefa3b82f535d7ea598c297b4471a937c4/av-18.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5548b79e2bf1f59b3e9aedc918a72d9dc45b9adaac10ff9470d5dbdda0002e47", size = 37895673, upload-time = "2026-08-12T22:28:03.98Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/04507c57249b399c3e4f23f01d221532f357338b5316fd2858fbd343127d/av-18.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7ea063f6690193ea335a1d592d6e0274350d45e2ed6af83ee107cb90cbfd84f", size = 39992431, upload-time = "2026-08-12T22:28:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d6/bc4b95bea9c2353a7e4d62a3fcfad9adcf0f881741c6ce01ee179d539ce3/av-18.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e4d48b9f12cad009cc72fe4f4099107de5e819c95f82767f4fd01a01481c0661", size = 28497798, upload-time = "2026-08-12T22:28:13.003Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d2/0c277a46f12647c1833f40496e132fb6001e0d19e6144b5ea30896461feb/av-18.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5cd9085028902c9880622bd37a12fd4b33060f06a52311f6f4867ca9f29a2c3b", size = 21421979, upload-time = "2026-08-12T22:28:16.48Z" }, +] + +[[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 = "claude-agent-sdk" +version = "0.2.139" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "mcp" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/b6/cfcdefed1f866a8ba372ef3884c8020dd54338d15d8b45d5a1ff7432cea1/claude_agent_sdk-0.2.139.tar.gz", hash = "sha256:4395ed541cdd4c13aeb1213b3b414b7e8a94cc060a773137e961882e81c174a7", size = 319519, upload-time = "2026-08-14T22:34:48.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/7f/f04c33553cbc69bb96d045dc38a6266726fad72130f22f405dfe9eb54bf1/claude_agent_sdk-0.2.139-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbc50cc475ec633cabfa36347646097e9b1466d53130e4a04a87308ff830c87b", size = 88043656, upload-time = "2026-08-14T22:34:53.027Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/a17f5318ca0220479f20fdf83fa54a838a0a13ee203495ff67c72c3f43a7/claude_agent_sdk-0.2.139-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:1c08206b1603444582cd365effaf95d2a8248661f1492281fb2d529b0887c047", size = 93000433, upload-time = "2026-08-14T22:34:58.225Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2e/5bcec31700d76ad2d5b9fc28521a75a66f464063dac11373cf8d61446a4f/claude_agent_sdk-0.2.139-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:e69ae1a0b2af684c64839cc16e10b70800d9d2f57622b8c0d1739dd878cd7346", size = 97396659, upload-time = "2026-08-14T22:35:03.734Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7f/582b3c1936c9f4ebc1bdc55a3923f1b680ef3c01928ffff1ea38eb84f637/claude_agent_sdk-0.2.139-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:34b289b3436fe24013f7b9cfe9f0a4e0806917a9ef8bbe829cda9a7b12d41a77", size = 98391889, upload-time = "2026-08-14T22:35:09.683Z" }, + { url = "https://files.pythonhosted.org/packages/56/54/d94af31d19b4e8d63d1b15002fd333ea77a040b7ab7a388044e511c8f9f6/claude_agent_sdk-0.2.139-py3-none-win_amd64.whl", hash = "sha256:9b76f0ffe216d6ca290d5f4f295ecb030dc496f101986ac99480a89d4abc6426", size = 100746507, upload-time = "2026-08-14T22:35:15.144Z" }, +] + +[[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 = "colorlog" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151, upload-time = "2026-07-23T13:40:40.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239, upload-time = "2026-07-23T13:40:39.562Z" }, +] + +[[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 = "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 = "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 = "eval-type-backport" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/15/273a4baf8248d6d76220723c3caf039d283774b31a7c46ba686120145b76/eval_type_backport-0.4.0.tar.gz", hash = "sha256:8397d25e6524c2e67b9576bb0636be27dea2192017711220c534ec2de921e9b0", size = 10260, upload-time = "2026-06-02T13:22:06.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/a7/bb99bf5e6f78736ddb53480f2c3ff3702ffe2196a7c5e1661c03081d398e/eval_type_backport-0.4.0-py3-none-any.whl", hash = "sha256:ad5e2a8db71b6696a56eafb938b0f5a337d3217f256b8e158b469422b4772b20", size = 6432, upload-time = "2026-06-02T13:22:04.827Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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" } +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 = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/b2/731a6696e37cd20eed353f69a09f37a984a43c9713764ee3f7ad5f57f7f9/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a", size = 516760, upload-time = "2025-10-19T22:25:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/c5/79/c73c47be2a3b8734d16e628982653517f80bbe0570e27185d91af6096507/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00", size = 264748, upload-time = "2025-10-19T22:41:52.873Z" }, + { url = "https://files.pythonhosted.org/packages/24/c5/84c1eea05977c8ba5173555b0133e3558dc628bcf868d6bf1689ff14aedc/fastuuid-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470", size = 254537, upload-time = "2025-10-19T22:33:55.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/23/4e362367b7fa17dbed646922f216b9921efb486e7abe02147e4b917359f8/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d", size = 278994, upload-time = "2025-10-19T22:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/3985be633b5a428e9eaec4287ed4b873b7c4c53a9639a8b416637223c4cd/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8", size = 280003, upload-time = "2025-10-19T22:23:45.415Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/6ef192a6df34e2266d5c9deb39cd3eea986df650cbcfeaf171aa52a059c3/fastuuid-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219", size = 303583, upload-time = "2025-10-19T22:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/9d/11/8a2ea753c68d4fece29d5d7c6f3f903948cc6e82d1823bc9f7f7c0355db3/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6", size = 460955, upload-time = "2025-10-19T22:36:25.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/42/7a32c93b6ce12642d9a152ee4753a078f372c9ebb893bc489d838dd4afd5/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe", size = 480763, upload-time = "2025-10-19T22:24:28.451Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e9/a5f6f686b46e3ed4ed3b93770111c233baac87dd6586a411b4988018ef1d/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d", size = 452613, upload-time = "2025-10-19T22:25:06.827Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c9/18abc73c9c5b7fc0e476c1733b678783b2e8a35b0be9babd423571d44e98/fastuuid-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7f2f3efade4937fae4e77efae1af571902263de7b78a0aee1a1653795a093b2a", size = 155045, upload-time = "2025-10-19T22:28:32.732Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8a/d9e33f4eb4d4f6d9f2c5c7d7e96b5cdbb535c93f3b1ad6acce97ee9d4bf8/fastuuid-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae64ba730d179f439b0736208b4c279b8bc9c089b102aec23f86512ea458c8a4", size = 156122, upload-time = "2025-10-19T22:23:15.59Z" }, + { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +] + +[[package]] +name = "fi-instrumentation-otel" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/ef/59bb1bf57a147d5badb0b35fc51536033bbe589a2c70504bb832a20ac316/fi_instrumentation_otel-1.0.0.tar.gz", hash = "sha256:5834cb77874947cbe2cd97ed49dd72d709f61ce8b3c4e159bbda8b918aa2d2ec", size = 43509, upload-time = "2026-03-10T13:50:01.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/9e/c7646340776f38d47838612b75987038bac740e5bb879cd52ef97d669cae/fi_instrumentation_otel-1.0.0-py3-none-any.whl", hash = "sha256:9af36b35c122e8be57d6834706314cc285bee84b8e2b2b3b074f55cb4d074006", size = 48558, upload-time = "2026-03-10T13:50:02.643Z" }, +] + +[[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 = "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 = "gepa" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/56/925779e5690971f1b022f7d107caf015c33ec09560261273ec137e23a8f2/gepa-0.1.4.tar.gz", hash = "sha256:6dd153a676ae5481764860d19286a9c0e8ddb5ef70d7f13044faf24978bdb6b8", size = 351343, upload-time = "2026-07-15T14:53:59.929Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/77/5b3a281cfd9caaa9e68349b434cf27f1ca448003ee0067a1ae2184dc52d1/gepa-0.1.4-py3-none-any.whl", hash = "sha256:12b971039599625c156d2231f6d72a29c31a22e9c237689459b5f1a3c353f532", size = 290167, upload-time = "2026-07-15T14:53:58.422Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/7c/9be3903e3d45415e8ca493c75f8990a0f6f579d168015d44c379350d0ab0/google_api_core-2.34.0.tar.gz", hash = "sha256:98a779fe72de956eb1c9c2f47ff4c4432a668ece1a002ec38bed07ec2698ae59", size = 187953, upload-time = "2026-08-06T06:23:58.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c1/a8a92ae1bc4b1a8f804c776d7d3f0c771b78a62c3ad4df1be41b3fd8c767/google_api_core-2.34.0-py3-none-any.whl", hash = "sha256:cdf9c67e7ca2402d86ccbfde5f2503fc83e3cc3f58cc78456ae96cad24a6d2de", size = 180545, upload-time = "2026-08-06T06:22:47.502Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.56.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/4c/fa42116a48bab3f7a143cf5042ecff7df9c8b73f8a376203cd534d1dc966/google_auth-2.56.3.tar.gz", hash = "sha256:40e229fc901f0a305b553050e5fce562d509bee0435be053abfa91582b51b90c", size = 367110, upload-time = "2026-08-06T06:24:01.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b3/6117b2f24065cd7e2c4f140e9a193e215f089ca8ba314cf91eb9d0b7fe0a/google_auth-2.56.3-py3-none-any.whl", hash = "sha256:8ec438808f813ad034535000261eed1067475d229d05bbf4216e78c3f2362e53", size = 259116, upload-time = "2026-08-06T06:22:51.788Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-cloud-speech" +version = "2.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/c1/5dc9795314f4aefea0b01b02e9f5486a198341ecc15fe47f89a61c68df63/google_cloud_speech-2.40.0.tar.gz", hash = "sha256:e89e688e4ce0b926754038bf992d0d0f065c5f1c3503bb20e6c46d08b63658fc", size = 404366, upload-time = "2026-06-03T16:13:59.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/78/afeca8d597fab54bdd823f857aad15d6f9c4628ff3cb72aa237d01700721/google_cloud_speech-2.40.0-py3-none-any.whl", hash = "sha256:7cc0302b3b9ca33d2eae9669da94a44316601a240942895362ac70e765b9f39c", size = 345427, upload-time = "2026-06-03T16:12:40.909Z" }, +] + +[[package]] +name = "google-cloud-texttospeech" +version = "2.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/bf/42e7d2f32d79c75bc23f949e97a278d46b7d08638e94b570947be02e3bce/google_cloud_texttospeech-2.37.0.tar.gz", hash = "sha256:db726382f393ceb6b36002c35abd62b53c4d8e17fc2f31df8b07fd0fabbe4f8b", size = 196465, upload-time = "2026-06-22T23:22:37.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/44/675358f0b939f14ae6481dddecbde5fd91b92b51f7991a70d98d3c68c02e/google_cloud_texttospeech-2.37.0-py3-none-any.whl", hash = "sha256:911f42f327027975d7781efcace1993afdf311b692b95b6814b71085750cc38a", size = 199697, upload-time = "2026-06-22T23:20:37.235Z" }, +] + +[[package]] +name = "google-genai" +version = "2.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/e3/1dd592243a0dfc3487ddfff7995f12686d0557ec953173dd9bdbeba2cb96/google_genai-2.18.1.tar.gz", hash = "sha256:a1e2be75c16234adc6641afd1ad4dd44218c9eec005d938bdc428585a048918a", size = 659694, upload-time = "2026-08-13T22:13:50.226Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/7d/5310f7a1cf290a6cb22eab37059610bf338ab7595892902591d2220cf825/google_genai-2.18.1-py3-none-any.whl", hash = "sha256:36a5949233e64a60f6cc4521bff7a76b7c569d0aa227bbe9fa642213b8a3a3b2", size = 1051129, upload-time = "2026-08-13T22:13:48.083Z" }, +] + +[[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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 = "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 = "grpcio-status" +version = "1.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/fd/848dd7e009de85f8ca59999d1cc618ff8ebf7ea5636d083a47455d212d24/grpcio_status-1.83.0.tar.gz", hash = "sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae", size = 13965, upload-time = "2026-07-23T15:24:26.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/00/73204406228cf989bea6b0fd9fe4702fab49a8a152a0c6f90856dadb6ac7/grpcio_status-1.83.0-py3-none-any.whl", hash = "sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69", size = 14636, upload-time = "2026-07-23T15:23:49.044Z" }, +] + +[[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 = "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 = "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 = "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 = "8.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/72/c600ae4f68c28fc19f9c31b9403053e5dbb8cace2e6842c7b7c3e4d42fe9/importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee", size = 56140, upload-time = "2026-03-20T16:56:26.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f9/97f2ca8bb3ec6e4b1d64f983ebe98b9a192faddff67fac3d6303a537e670/importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f", size = 27220, upload-time = "2026-03-20T16:56:25.07Z" }, +] + +[[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 = "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.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" }, + { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" }, + { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[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 = "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 = "levenshtein" +version = "0.27.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rapidfuzz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/d9/5acd910cb9527d6aeba8dbc0a5d1093e72921f2d0ca586a4594660615688/levenshtein-0.27.4.tar.gz", hash = "sha256:3df1c12bf5e485774d6387f3894271ef3724414ecc20dd238ae4d2333e093c83", size = 401198, upload-time = "2026-08-08T20:27:04.375Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/05/e04f67eb6d92f06c4103952e8990f22fbec36c2ad557d9801d12a4ce9f12/levenshtein-0.27.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e83fb7ab79a0c7d2fa03f5066640e9185b54faa6e0bc1c5d927e3ea1b7708c32", size = 174708, upload-time = "2026-08-08T20:25:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/a1/52/69eca1e05e07ecb84d03f5670fcae8021a44d1d2954294f83c02bbaadb91/levenshtein-0.27.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:713664defd108ede005c311de30a14d32e18a86b78e4d64bab3c9c2048275dba", size = 160704, upload-time = "2026-08-08T20:25:20.468Z" }, + { url = "https://files.pythonhosted.org/packages/c8/68/6cb3cbda477c230b6b78a2c6ab904eb57f1c317538be33f65319b10e93a4/levenshtein-0.27.4-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e12e84cbbcb17d2764eb3e62d23338c5c4ab3778c10395ddc9236bdfec96c98e", size = 137900, upload-time = "2026-08-08T20:25:21.741Z" }, + { url = "https://files.pythonhosted.org/packages/48/fc/2305ee9affdcc3fb36e220161802aff457d4e54b957bf1263a6d97679744/levenshtein-0.27.4-cp310-cp310-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:627613f15f9a3a4a81db9a659dae9e768e794272b08afdf23522faef73fa6481", size = 117424, upload-time = "2026-08-08T20:25:23.431Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/85c17c56d6647261214236ea3b2e32567ab1605a0efc2c2fa365c1a2c1ba/levenshtein-0.27.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c910164fca400d7bf320802831e2df4bed40b95497cdaca825d49b2838b940cf", size = 157678, upload-time = "2026-08-08T20:25:24.822Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/bd85dc464c2a5669c63edc642ccc3fbce773b0e958aa69d361e50bee0c38/levenshtein-0.27.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac57c23e6c45cc1d60eb957acb01e4b733c45d9241498a24cfa586d78359ea1c", size = 1122166, upload-time = "2026-08-08T20:25:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/21/58/c1c918b5fa6e5a48d38039dc177483a07965e55ae9d6bd2517247e9ebe9d/levenshtein-0.27.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:2ca024d6d0a33943cce814219809ad43d03ee58e9a0d138cbf5fc4fe127093cf", size = 1011279, upload-time = "2026-08-08T20:25:27.756Z" }, + { url = "https://files.pythonhosted.org/packages/e0/09/55b9b5f91be79776a4de0f6674afbd09fe164b4bf48738d0565b756eea2f/levenshtein-0.27.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7b13425d4e4bf41ed1bc54401a59b4c7ede3b781cebcf95d5ba4dd2890f12dd5", size = 1190398, upload-time = "2026-08-08T20:25:29.233Z" }, + { url = "https://files.pythonhosted.org/packages/73/19/40171c5e2412d178cbf0aaa665dcdde281313455efa45a103c83d13d5075/levenshtein-0.27.4-cp310-cp310-win32.whl", hash = "sha256:7fe41767fdd102f50843fcc52458b3e0376a53b97f736abfb2ffccd56de5a92d", size = 274713, upload-time = "2026-08-08T20:25:30.844Z" }, + { url = "https://files.pythonhosted.org/packages/36/86/034c37e20118a921f5ae9f942d8e7227e6b3fa2bf72dd4700ac3fff556d9/levenshtein-0.27.4-cp310-cp310-win_amd64.whl", hash = "sha256:e2c69129ee68b376d7fb8e14bd33a5cc5a6548aefee6eb3b37c84f751117126e", size = 283181, upload-time = "2026-08-08T20:25:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b2/3d3d07d2ce8a6ea8d1feede47fa37944fdcc141909f8994bb221f47d9c7c/levenshtein-0.27.4-cp310-cp310-win_arm64.whl", hash = "sha256:f7af9248c56433fafeeb3e62897c2ff4052d8292066d499e2ae35242b6e1f4bb", size = 451167, upload-time = "2026-08-08T20:25:33.703Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/48a036d38c9a2ca8ceee0684b389fa1ccbdcd12cb050ec20e43f5e40e6b6/levenshtein-0.27.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5935dd0e5ee6eede5c5879377f938f3bff7bbe25d48b9b87fa9d907215b247a", size = 174631, upload-time = "2026-08-08T20:25:35.626Z" }, + { url = "https://files.pythonhosted.org/packages/6f/26/aa418f8e242da2f92e6e69ff8393c30c995fc7ff551f4079236474699694/levenshtein-0.27.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:852955f6e3d9edcd365363b9a9ba97ae96bfb29acf923217b29aab6b38f87b74", size = 160571, upload-time = "2026-08-08T20:25:36.925Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/e62f758b306d0b148e3c57ab22b17274ec227603b9cc940d61491c950593/levenshtein-0.27.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3d2a355cf9d8c48058349b594841e3c60b5abfbb5de98077b9eb2b1e757fff8", size = 137922, upload-time = "2026-08-08T20:25:38.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/6d/1a8272ebfa7f3513108f07cecec8b48dc44ae867f757fd7123aa4740289f/levenshtein-0.27.4-cp311-cp311-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:571a8b7d30c59b3e033c883c1486c88bc7515cd49d866abd5c9bf8391fba2ac5", size = 117472, upload-time = "2026-08-08T20:25:39.566Z" }, + { url = "https://files.pythonhosted.org/packages/26/14/8bab43fcc1b6e2c534f9d4a3defc03035f890f90f73c16fafbebfffe3658/levenshtein-0.27.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1e57bf29324e16b07949b917aa0655c826f44896c3e4db6af7b7d9770b7ee30", size = 156991, upload-time = "2026-08-08T20:25:41.024Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/df78e0fda7e90de1f69b311a009e496fcd167045167cc4a14c74379f10f7/levenshtein-0.27.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:61e2eeadaf4503a95bdfa5821a82f8e41a840a03bde467f80162f046ebb388fd", size = 1122179, upload-time = "2026-08-08T20:25:42.724Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/3c1b6944e40234f858750d873c03b9774ba0d43c0a19ebd9f79cce0bf450/levenshtein-0.27.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:57d34c9680a5fcec8893f8613ddfcc9c542ca0e6b5e560c78919004ed57b480f", size = 1011228, upload-time = "2026-08-08T20:25:44.329Z" }, + { url = "https://files.pythonhosted.org/packages/ff/61/409e98f64ec8d37a5514ba04aa4f327821ebb7f2322da8e35e9fe78b50cb/levenshtein-0.27.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5f4938fc14f2c83fc6945513fcecb4eaf34edc8820b2e6bd4d1bfa41d06b23f4", size = 1190139, upload-time = "2026-08-08T20:25:45.776Z" }, + { url = "https://files.pythonhosted.org/packages/34/52/1c962d4e7fbec06d395e42d23b2a628669dccb055df5d5ef72915b6b8385/levenshtein-0.27.4-cp311-cp311-win32.whl", hash = "sha256:867dd5afac5063e59ef2038e281d7cfb865b04a002cdd012c23b7b19dbb5be0e", size = 274624, upload-time = "2026-08-08T20:25:47.143Z" }, + { url = "https://files.pythonhosted.org/packages/99/92/f29f586df972b4a04b4a343609d7e1421150ed8f7cd50982bd3f711c61ee/levenshtein-0.27.4-cp311-cp311-win_amd64.whl", hash = "sha256:b24df629ce4bccac4bbcf1933be092acb74a7bb85ca4b60fc47a7e6b156f9a4d", size = 283265, upload-time = "2026-08-08T20:25:48.649Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ba/e78e2e29c080579d79e216c951e5663a7eb1b214d961f1b5d7665b32acee/levenshtein-0.27.4-cp311-cp311-win_arm64.whl", hash = "sha256:4123b8eb65048f51146d8d450ec6dc1c35efbe7b26707b5fd75b8bfd3987ac02", size = 451169, upload-time = "2026-08-08T20:25:50.121Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/f98600264421adecce98d6af45881829cfd2c9cc5e217dd8728c757e53f6/levenshtein-0.27.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75a77c97b3548cc7d244af1c0aeaac1d3182226e4f94e0ff0935789c965398d9", size = 171953, upload-time = "2026-08-08T20:25:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/ff/14/434903134f537705b2df28e41be86a8dc8cd7fa39969998ff9010f5e0a76/levenshtein-0.27.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1bd46d86c6f3558136c5e868ae3d99d9bbbe7f07f05162044f53571ceabf532d", size = 160975, upload-time = "2026-08-08T20:25:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/66/de/9b5cbf4fefa53990ae77f62a1dbcf70082bee269a30b7015822f885231e1/levenshtein-0.27.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43d038019f592e54f65c857b36d91d73843ebef51a8fc04d0ad32d322ec7fc16", size = 138463, upload-time = "2026-08-08T20:25:54.502Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/4577747a09af2424da34da91742ff852baaa8c2b517fe88dde6d6aef5173/levenshtein-0.27.4-cp312-cp312-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a5daa4a0a9ae82c2800e9ed2df4ce339d21976fa0de68be0e904836b4069d744", size = 117768, upload-time = "2026-08-08T20:25:55.852Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a3/34ce55c6f6accc3f827601635e667b592cd3abb1d93171dcaa0021608f57/levenshtein-0.27.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b2b97dc4986616e857df440a2a3425525a874b3a3cecea8a6316a73a14d5cec", size = 157588, upload-time = "2026-08-08T20:25:57.125Z" }, + { url = "https://files.pythonhosted.org/packages/be/a8/5ec573866fa96d55eb25433b5ea7e71d64e448b60c6b3f1301879a0e19d6/levenshtein-0.27.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9da450f1bc0d860c8796b7be0a19a2f3c38eddfbcc7fcc2bb6eeb95f8c16d86c", size = 1121452, upload-time = "2026-08-08T20:25:58.701Z" }, + { url = "https://files.pythonhosted.org/packages/8e/de/f8559c12e88f483ba64e06719d7d8dd0593834c6aa1c424cc044cbcdf4c7/levenshtein-0.27.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e2061d8aeb940762bfa6f75face75b0c88cad44c3066815ca4038f6720f9b2b1", size = 1012030, upload-time = "2026-08-08T20:26:00.194Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2d/fb08fcd2fa70275d4e5affbb08aaffad854a544a6a8a851ecd8d8b8d7ff4/levenshtein-0.27.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0c43c9c08aa0f40f5329300838e412b15cd6c4777edd081cf8fd5b3d8b1532c0", size = 1190967, upload-time = "2026-08-08T20:26:01.779Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ad/df9fb70048365871b3f3d850471e351a7ba1c448dcb5003b27519ab4d5a8/levenshtein-0.27.4-cp312-cp312-win32.whl", hash = "sha256:c9956028bf43365f52fbba09ace0a88cfb9e1dac5bfc4251ae78f2c1b5232597", size = 275029, upload-time = "2026-08-08T20:26:03.356Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ce/7860a34c60a4881cd66afebbe990030ea840642146acdbe8093a5be7f8d5/levenshtein-0.27.4-cp312-cp312-win_amd64.whl", hash = "sha256:b6b4e609d558ce8cc5265f101a5961339746311e40827f8c3e4474b4b7a1529b", size = 283650, upload-time = "2026-08-08T20:26:04.656Z" }, + { url = "https://files.pythonhosted.org/packages/c1/76/90d2e98c4524e9e8b210b681f14fc1600d5c53a2293ff20b8c56cada7792/levenshtein-0.27.4-cp312-cp312-win_arm64.whl", hash = "sha256:e617740f81adf395efacbaa9e78257ea231c42071a81732de568295cb775c0bd", size = 451547, upload-time = "2026-08-08T20:26:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/83/29/07c9ca71cc211b4e97838f54c4e759b8aed5bcc8f1a8360eed1eae2e30cf/levenshtein-0.27.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8251a1aa9e8a5f44fce1a568a21778c78dc68b55ef12a211218e05d57bc064ec", size = 171541, upload-time = "2026-08-08T20:26:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ce/d0fa10b5359128261ae1c5df37b77f8b70a94d581e707109ca789355e835/levenshtein-0.27.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f139f585222e6035086c56b797f7b10fbbc836d18089a4dcd29c812e7644836f", size = 160358, upload-time = "2026-08-08T20:26:09.064Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6b/04e9548789afc6e4837b67dcb5910a21a646af2c8d2f41796f3e148234e6/levenshtein-0.27.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04b1e1c8beb018ccb9488dd2735bd3a363f38038a84cb639fc5d9fc8234e3905", size = 143222, upload-time = "2026-08-08T20:26:10.445Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/21e7d35a0edb5964c24f0c4508f97e8986b154a817276705273099c495f0/levenshtein-0.27.4-cp313-cp313-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bf47cc38c8d58640e8d36905725cf8311f4e227412fa978df791d52bee4c6c54", size = 117327, upload-time = "2026-08-08T20:26:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8f/09ce50ce6aa3fe9504f746bb43fd7813cabfc66733ee89db1a4d8e6ed0e9/levenshtein-0.27.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f54c2e48821f55d364d976126be5b37fb91e147d4bac5eab0a17459a5bbbe20", size = 158722, upload-time = "2026-08-08T20:26:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c6/4f309a6a1b2338e60c4f292b64e1ae4e610b912a717d97c6648766c26f39/levenshtein-0.27.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:de0a4beb4f821aa0bf8dc8529694ba77dc3cbdf47b4e8805fdd3289482bfeee6", size = 1126812, upload-time = "2026-08-08T20:26:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ca/b6435c0e1ede23b2c46960250400e1912c34a59b9b75d0510afcbdf56b4b/levenshtein-0.27.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a6f1bd018880093899ff7ac72edf05984a1c0504e75146a650bc8d282a79a992", size = 1013120, upload-time = "2026-08-08T20:26:16.02Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/b18ce73769f556a029e93a5ea266a1af26005adc6a06097fd55d380c9ed5/levenshtein-0.27.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0c27275a04f7771a70808e57e4630ac4357de7510c899c4be4ee933debeb7c", size = 1193215, upload-time = "2026-08-08T20:26:17.725Z" }, + { url = "https://files.pythonhosted.org/packages/24/61/94c91d91a0b24dbd9b37f3df3ce4169dc3669e232fb6c8837836dc7c7650/levenshtein-0.27.4-cp313-cp313-win32.whl", hash = "sha256:02f9fd7a90fada0b0a66a16dc854fa60a466c49aeaf289c8a94c6cdae4150b89", size = 274872, upload-time = "2026-08-08T20:26:19.409Z" }, + { url = "https://files.pythonhosted.org/packages/49/52/c8326d0a74216ca1f6a5251f319722b15d11d8868406f3742176b29ee7fe/levenshtein-0.27.4-cp313-cp313-win_amd64.whl", hash = "sha256:d364163c93bbacebdc18a19b9a9f0bc8f0b9573e48be056f01944ddfbe4d90e5", size = 283307, upload-time = "2026-08-08T20:26:20.783Z" }, + { url = "https://files.pythonhosted.org/packages/82/77/0a4e4a799bd9768dd6d89301051efa8b3a75f17e1e18b0b6f460ea2888a3/levenshtein-0.27.4-cp313-cp313-win_arm64.whl", hash = "sha256:18e4f373634940e202bb9517254a8b475afd50505c6bbf0844407c2bd08355de", size = 451014, upload-time = "2026-08-08T20:26:22.159Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/67b04e69022317a273b36aca7be7ef7a739499b1f836bf6bd612562e3b9c/levenshtein-0.27.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:38b1f8c34dacd5b5ad5ab9c361039370e35a2f79b9b679c25f11534290207a43", size = 171858, upload-time = "2026-08-08T20:26:23.471Z" }, + { url = "https://files.pythonhosted.org/packages/67/72/4ec7b6e5472e4958c5ed70f9217bb3e7573f62bc7d96df61f03a957685a6/levenshtein-0.27.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61d484fc0e8e4f5cc06b7ddded68751bd9fadcd14ba2b02ee6fe173bdf081a16", size = 160573, upload-time = "2026-08-08T20:26:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/7e0f93fdab7570c985054587868d3444fc2eb96731487752e6e730e49035/levenshtein-0.27.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b10a8390c9b3c9bdc2400ba2d79a20c19c94c54b2f4e8976c9d2319fce891bf5", size = 143603, upload-time = "2026-08-08T20:26:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8c/624006e490115983454095b831d479aaea0a1558a7dceaccaedb029a6c31/levenshtein-0.27.4-cp314-cp314-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b570ced6ae4ce35709cf06dc1c4f8cf623b87354ad838c9d15284373ab3ebadc", size = 117159, upload-time = "2026-08-08T20:26:27.702Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f5/125c887aa05298af8a3930db5bba2b51ade0d6a974bf77c546df28edabfc/levenshtein-0.27.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d36b6e6e5ca14cde39f7aed2af825038bf9b4e7bfc46b9a30c4d8dd4f9b22b02", size = 158878, upload-time = "2026-08-08T20:26:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/2f/21/8ef976f38b3bed6dc634ba5818288a65a8728bd4cd9285deca93e4725b5c/levenshtein-0.27.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:18e8c02c5c9423615906eb7e95803e1ba857701c7fe6101da0df4b45bb0467f5", size = 1127214, upload-time = "2026-08-08T20:26:30.766Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/f6d97fe832d498543e10f5b3dd72bb346025dfe4696788d78fe1b123d996/levenshtein-0.27.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:667b975f05f8f845bd6158ccf82bf0aa8912b9e88fe7800d49ce88e413f9aa61", size = 1012576, upload-time = "2026-08-08T20:26:32.368Z" }, + { url = "https://files.pythonhosted.org/packages/6f/67/2baff7e5e459b2ccf968213cccd248248b3c486af42769f59dd9010ff000/levenshtein-0.27.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5a630503786a14b11a0e0fbbfe7ae91189f54e357fab38a7bf5f5ec3a838e37", size = 1193361, upload-time = "2026-08-08T20:26:33.943Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6f/b3189be0480431b432ab6e70feb7662583dc50b20db37164811be3a4cca9/levenshtein-0.27.4-cp314-cp314-win32.whl", hash = "sha256:5951795b568cc756ae61b7fbcc9f8cdc55c4476a43fdb9fef4f8cdb4b4cd9e3e", size = 282218, upload-time = "2026-08-08T20:26:35.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3d/595390b5d34bc1aa0b5e9361d3e1e651f0836d69cd21a261dd9725c809bd/levenshtein-0.27.4-cp314-cp314-win_amd64.whl", hash = "sha256:a9b1b25d559c2c322603e3a5edaa2beacb156c34fd5ee78351d49963aa0d0764", size = 292369, upload-time = "2026-08-08T20:26:37.137Z" }, + { url = "https://files.pythonhosted.org/packages/23/4d/412b3406d56d1db29e5432c196f6f2e2577046a53dabce53ff5f223da1e1/levenshtein-0.27.4-cp314-cp314-win_arm64.whl", hash = "sha256:64619c1674a8eb37dbeda7fe397a858d3cf6447d1a1c479d37ce4165bbfd3971", size = 467353, upload-time = "2026-08-08T20:26:38.8Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fd/801b3c6d40598a46aa28218fd5e1daa12f8fff9820a92e7cd99decb1ce20/levenshtein-0.27.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6b380f5f0aa3bc3b80b4b91ba57bf38a8b1e1dc087a1c4677a55c5ca6b2cfdfc", size = 173601, upload-time = "2026-08-08T20:26:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/8e/38/c6567d3c0988580db1f77a434ec8c9e54cd84cbfa3560aeb9c2ec71844fa/levenshtein-0.27.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:34615d2b8b17471dcb7175a0e4ebe355a0791ce364bb9470c559d24c831c9bea", size = 162272, upload-time = "2026-08-08T20:26:42.065Z" }, + { url = "https://files.pythonhosted.org/packages/08/83/64230baa4ccdd6b5c2bf9f5d8373a2cc5689b293a5812d72089468b5c991/levenshtein-0.27.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23d58bf0089a737ec982e259da98a8856d80dda13b05ee0baab01b0596d6b145", size = 138003, upload-time = "2026-08-08T20:26:43.505Z" }, + { url = "https://files.pythonhosted.org/packages/a1/41/261a3cbd6c738b887994795cac8ec79c922ac6a77de882eaea4dbe606c0a/levenshtein-0.27.4-cp314-cp314t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4b3830767fd2a9b68698be458397cee4cec409c8d8992074baaffc0576a89d10", size = 119900, upload-time = "2026-08-08T20:26:44.897Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/62e569a4fd4ca3f2269d100340f9b74dbdce5e0c75fbbd48a752182b4d77/levenshtein-0.27.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a410d7fcdbd76ee483d02e5825980c3d06254c4001d95d251bfaf9c14477b4e", size = 157701, upload-time = "2026-08-08T20:26:46.326Z" }, + { url = "https://files.pythonhosted.org/packages/4f/10/8d9fd5b30fccb792cc0888df40f325d276727d524845b42eec33e4e351ba/levenshtein-0.27.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f04ece487bf4212d91784a476533a5bda1701f71a12e50487869dc42787ea30", size = 1121144, upload-time = "2026-08-08T20:26:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/83/4a/7ba1add64eb6e964b1ac47ba0092284cc762e92790257803cc5a9cd6af0d/levenshtein-0.27.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8e5b53fa96318beb2a5d72be3d6cac23759eaacf537843e3e1a80ee345dc5b9f", size = 1005066, upload-time = "2026-08-08T20:26:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/de/c9/aac4a4749a2b04078371a06acc931bc3bb871640a3572ab7d4d4c83f0429/levenshtein-0.27.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12379e482aef24b0a3c38b41669b88f8c9edcac7301367dc52449071f0d1a2e1", size = 1191692, upload-time = "2026-08-08T20:26:50.902Z" }, + { url = "https://files.pythonhosted.org/packages/65/11/0fce9f771f6decefaa0a814b87f3e8d42d382b59af2176fdae805c244a2b/levenshtein-0.27.4-cp314-cp314t-win32.whl", hash = "sha256:7d180894a008367953cf076525421a5488619ce1c4636c9d34b1c836bf8bee03", size = 284631, upload-time = "2026-08-08T20:26:52.558Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ad/3d6c65d5bf7094c953ea0358b1499d62e935d93c3fb9c3b324cbe0711706/levenshtein-0.27.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8ff7e95d8fff15a0889e2305274b089cb4726bd545952e17e06382a3a9da083c", size = 294923, upload-time = "2026-08-08T20:26:53.987Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/fd7b0982024b1b902f301c297b9be38214a43ef7781141879bd3aeac031e/levenshtein-0.27.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4416e14abcae7394647b0c57aa2ca00d96e99c53c4a1a4cc23745f8eb7412909", size = 469057, upload-time = "2026-08-08T20:26:55.8Z" }, + { url = "https://files.pythonhosted.org/packages/be/89/16e57de52092e0d4b043a229b635e606501f4f0b981bd7281debc7898b1b/levenshtein-0.27.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2341edccd51adc715bd34efa274e90d353d1917f4a740656a66fe0280df8e77d", size = 168582, upload-time = "2026-08-08T20:26:57.156Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ab/25a0466ad919e69c44f908cb1c8b74e54c3142781ef3c7b99f37f7bb7eef/levenshtein-0.27.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:cf230f6f59ae63e7383beb57dd50c8a0ea84c9aad0904e677c7cde32c72c9c57", size = 155879, upload-time = "2026-08-08T20:26:58.528Z" }, + { url = "https://files.pythonhosted.org/packages/71/5e/cd9e0f2dc9212244dd3e5a199671d19dfdba15c1de2fdef7d713096cbaa7/levenshtein-0.27.4-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ffdd22f8a55d47d9de20ec17b169369f865f7e60770b8489ef3f68dddee3e6b", size = 134049, upload-time = "2026-08-08T20:26:59.945Z" }, + { url = "https://files.pythonhosted.org/packages/15/9a/fc8a08d5478694b800bbc6dfff4a1df26637b136a35d2523920ae214f748/levenshtein-0.27.4-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3332732a1b47c4e5538271fa3545377703c70a24bbe18888c1a2e5a3d9a86fc5", size = 154421, upload-time = "2026-08-08T20:27:01.314Z" }, + { url = "https://files.pythonhosted.org/packages/03/d4/a13a7e48ab9cd0580918dafae045e5ee261bac5b4b072612daafd9408621/levenshtein-0.27.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:39676fa9fdb625094daa8aad3969d33ced483cb0c5f3d81973cd0a78571e6be2", size = 297544, upload-time = "2026-08-08T20:27:02.847Z" }, +] + +[[package]] +name = "litellm" +version = "1.97.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/08/db78a9f53e5688ad0f9e50d5c3bfe616e445aac120eafcca907f04634e48/litellm-1.97.0.tar.gz", hash = "sha256:6f7ce326a2e5385ef850e0b0768d41f502ec79278860090a838511cea067b067", size = 17762282, upload-time = "2026-08-16T00:05:44.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/d4/a04f8bda468fb25a0a8a392a6922c9ecf6c122ad9bf9e2f69ebd173e1cac/litellm-1.97.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ff401dd5d66f54b9b474f0652c419fb7bf883fbf5ca64c0bc363acdc98b758b5", size = 24235645, upload-time = "2026-08-16T00:05:20.055Z" }, + { url = "https://files.pythonhosted.org/packages/f1/51/a055bf5df38112f07970937d95416b22379ee84664b739bfe87a8292e098/litellm-1.97.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:2983b40ed5d8b1bcbbfbc0d66fefa21b04db91b87c05dd680ae77cba74e561ef", size = 23893493, upload-time = "2026-08-16T00:05:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/14/28/b1cf429493aec5260ac2737dd82f6200c729fd26b336dc21cf001cfbcd8a/litellm-1.97.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e3a1f70d693716b4e8a8108f0a464b0e2c555e274ddbb8ef89c4c59c80be14ed", size = 24031743, upload-time = "2026-08-16T00:05:27.138Z" }, + { url = "https://files.pythonhosted.org/packages/08/4a/22c49e8bd068bfdab0daba235cc2d2752d76d855ccb8f6dafe7dabf01ca4/litellm-1.97.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5b56dce7df44a6a9e6caf5379de2578a8cb82831ceabd3d71cc99b370a1015e7", size = 24397362, upload-time = "2026-08-16T00:05:30.89Z" }, + { url = "https://files.pythonhosted.org/packages/0a/22/f60558230969ac7d860bd93aff6bf6a69bcd8a3ef4f35eea211174ef4e81/litellm-1.97.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6b360ddc3162c2ed39b64d3f9957a7af70cd9c60cf71f7a4dfa355a0bf05bebc", size = 24107078, upload-time = "2026-08-16T00:05:34.463Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e4/6c74ff4b188d9399a036d41e86c4c616d95ce5fd9d9b3c42646bd02f270e/litellm-1.97.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3c4f1dd45e14127f2303769a7ae79482697823e329ae503513d014ceea4dd704", size = 24493640, upload-time = "2026-08-16T00:05:37.85Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/1475c4ecdf43221ab8ee8d0fcc6b469c26d0d80913d0773e18de81cf18f1/litellm-1.97.0-cp310-abi3-win_amd64.whl", hash = "sha256:dce3377207234fc5c5b275a5e234ba056a5051fd178ae8e9a6aeb5d056f12095", size = 24289278, upload-time = "2026-08-16T00:05:41.441Z" }, +] + +[[package]] +name = "livekit" +version = "1.1.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { 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 = "protobuf" }, + { name = "types-protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/5d/bfaf1cc73f960b40294f604d334f05e628b0a07de3c47e475d760996a8d0/livekit-1.1.14.tar.gz", hash = "sha256:47428e10ecf20d7db4ee9fde4009bf96578c003b1ae6e1c5e7e4837a55902393", size = 375000, upload-time = "2026-07-31T14:05:14.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/ff/a2659522b3cf860b9b4453e1ec12d4b4c7e9cfd2b672f2cf925016d73492/livekit-1.1.14-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:5f671b1752c93b878cb241b84fd3f72a31f857c3927755d672cfb7656a84778c", size = 10196322, upload-time = "2026-07-31T14:05:04.167Z" }, + { url = "https://files.pythonhosted.org/packages/82/a2/89f32d369cc78cb1a50b2a9e635c653f88d86ea4338ccdfa7b2d4ca0aecd/livekit-1.1.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:efa16b9036b0b592e5399fdb858c1f04ec8a32c385184c705f030952f72174e8", size = 9019745, upload-time = "2026-07-31T14:05:06.49Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5b/dda7d660fa5d5b6e228dcfc6be3664a2442d1601481686052af2da642e5e/livekit-1.1.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:299146efefad5f67751cd15b8225bae759be0d7ad2f0b4ae1a22c15860d93cf9", size = 10042499, upload-time = "2026-07-31T14:05:08.563Z" }, + { url = "https://files.pythonhosted.org/packages/21/e3/d9255eeaf205f090d63d762e5254097b62af394bfaa90106f71f1fb6740e/livekit-1.1.14-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:80962c4a22ddbf0e0ebd3563fc090fce42df66b39b90de68b161b7db01970f68", size = 11445915, upload-time = "2026-07-31T14:05:10.628Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0a/514fb230e7c7f13ae7e53b9e39a6dd9ea1aa9ff5be9e588d55301d159a1e/livekit-1.1.14-py3-none-win_amd64.whl", hash = "sha256:b8f8d38f131956297923e520bc4375bc9ebfa255cab7f125cb7755bfca71df24", size = 10766643, upload-time = "2026-07-31T14:05:12.716Z" }, +] + +[[package]] +name = "livekit-agents" +version = "1.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "av", version = "17.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "av", version = "18.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "certifi" }, + { name = "click" }, + { name = "colorama" }, + { name = "docstring-parser" }, + { name = "eval-type-backport" }, + { name = "json-repair" }, + { name = "livekit" }, + { name = "livekit-api" }, + { name = "livekit-blingfire" }, + { name = "livekit-local-inference" }, + { name = "livekit-protocol" }, + { name = "nest-asyncio" }, + { 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 = "openai" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "pyyaml" }, + { name = "sounddevice" }, + { name = "typer" }, + { name = "types-protobuf" }, + { name = "typing-extensions" }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/83/231735a604cfb95fa24911832620babc808fbfff61a89f21b1e8bf2eef9b/livekit_agents-1.6.10.tar.gz", hash = "sha256:1bcdcca95414860a305f2de05ff14964e8a7e2e6d63888299f3c8b5d9ed02827", size = 2654392, upload-time = "2026-08-13T03:26:07.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/38/7e1bc833dbb6e2fa510dfd762ca8464aa958f8848a974d01dbbe73c117f9/livekit_agents-1.6.10-py3-none-any.whl", hash = "sha256:8ac82fc0e810d2e3e01fe4a1a632182d873a89e75e93d07da136eef23bc7cf99", size = 2769487, upload-time = "2026-08-13T03:26:05.394Z" }, +] + +[package.optional-dependencies] +cartesia = [ + { name = "livekit-plugins-cartesia" }, +] +codecs = [ + { 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'" }, +] +deepgram = [ + { name = "livekit-plugins-deepgram" }, +] +google = [ + { name = "livekit-plugins-google" }, +] +images = [ + { name = "pillow" }, +] +openai = [ + { name = "livekit-plugins-openai" }, +] +silero = [ + { name = "livekit-plugins-silero" }, +] + +[[package]] +name = "livekit-api" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "livekit-protocol" }, + { name = "protobuf" }, + { name = "pyjwt" }, + { name = "types-protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/19/36ff6712ec638a4b7dad4d8f03795952e401dc31db0b04cddec7892650da/livekit_api-1.2.0.tar.gz", hash = "sha256:a89817b3bca9584873786ff07209839308217537a42f95ecb2609aafaa109ddc", size = 20778, upload-time = "2026-07-11T23:20:54.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/e7/8926f16d4bc1b2e0ae46d4a507321bb899396d263a757f1adaabcd3b3867/livekit_api-1.2.0-py3-none-any.whl", hash = "sha256:307f8e5cfb0358c3ca091814ab768af55896022151bcd7f951954ccefa036a24", size = 26499, upload-time = "2026-07-11T23:20:53.736Z" }, +] + +[[package]] +name = "livekit-blingfire" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/0e/e1d79fb428ad43396da2ee4217ae043e42d75b4270e97e76d20c9d17438d/livekit_blingfire-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb8f6a9e69b0e58abd913e0b3b5f27bd79ae498887a9e6708c2255a6841a3f1b", size = 152217, upload-time = "2025-12-16T00:47:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e6/d881bc1bf61f4bd71df7b52e89a523b4046913977794dac2d2f0453151c2/livekit_blingfire-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:610a7ef7b1c81be587c41241cbdac474f8461345ee066330c69f7c460f81e7e0", size = 147320, upload-time = "2025-12-16T00:48:00.553Z" }, + { url = "https://files.pythonhosted.org/packages/db/81/714a5a4cc742856cf2077ac3851d943c2a4accb4ec76d291c9d8f96fe9d5/livekit_blingfire-1.1.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bf808159597d402415ae06cbb87e8cc8c2a58d2448e0fcd0ae3cf14b114f395", size = 165503, upload-time = "2025-12-16T00:48:01.818Z" }, + { url = "https://files.pythonhosted.org/packages/35/c9/fb8ca3881dcbea2d04cc8995e501a67a450fc93cda3ec4638608030b22f1/livekit_blingfire-1.1.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9fd97f49831c34065f8db3b1407e95c6c3353f0c35b6fff78547582d3d5278", size = 173081, upload-time = "2025-12-16T00:48:03.522Z" }, + { url = "https://files.pythonhosted.org/packages/40/2b/98ba07aae81eb87d426d2bf57426a0861f3f39c41c4d15158612c1d41fc5/livekit_blingfire-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:e747443f3b21999ec1d6d96c2f128dc8375937795dc7bedd8fa7b2a7e54d341c", size = 129305, upload-time = "2025-12-16T00:48:04.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/09/1095ace608a41810d5c0f343eff36154505487c415acd9c653a882ff2cf1/livekit_blingfire-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0358058ba6cba59379d22a01acef6ff8a729b0facf880c0f75d13c26f1315c9d", size = 153650, upload-time = "2025-12-16T00:48:05.976Z" }, + { url = "https://files.pythonhosted.org/packages/80/a5/f4eb0e5d97334581440d37ced2a1db4fdfc8454c641c7c144e858012f1ce/livekit_blingfire-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a0741a8abcfaa1f3af2313271f15ac0f79777681a8e3ab9a782a68d8eb121c89", size = 148628, upload-time = "2025-12-16T00:48:06.998Z" }, + { url = "https://files.pythonhosted.org/packages/89/f9/dc5ad008cb8b9c2a300bb7f7d44f022cd4970a32707eb90358290a07f0e1/livekit_blingfire-1.1.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d99d7a34c9350da3a6ea738bc282a5f5b4ac4ffb7f8aa5251dfa96070ad845f6", size = 166832, upload-time = "2025-12-16T00:48:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/408c435cbed31fa3601ff32ef0499ff594cd898b483c9b4017e9df906de6/livekit_blingfire-1.1.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:815aca6c2f823fa25d7a15d8d76ce18b0295aa5ce2c988ed64fdbd9c4d3ced0a", size = 173959, upload-time = "2025-12-16T00:48:09.153Z" }, + { url = "https://files.pythonhosted.org/packages/2f/12/c826a40b32bfda29e7f826e50dfbd3c0a70726cb8c0cb5023d2311823bd2/livekit_blingfire-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:7ae045d44d8cb867fc449f44a95c0287f6e5d225e62e24f4574bac8f26ede845", size = 130006, upload-time = "2025-12-16T00:48:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/dd/18/8be31c84e911218011e6e653ca466fef320a4e7bc926aa694bc4cb6625f9/livekit_blingfire-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d5fb6746263529b780dc8bf7a6e6a80ff5fa7fa729e403f2b925996d041e039", size = 154567, upload-time = "2025-12-16T00:48:11.097Z" }, + { url = "https://files.pythonhosted.org/packages/03/64/bb5463d4a6a97888d52caa6256d242acab1f7eabcc59343f7874a89a30dc/livekit_blingfire-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4d5642e36fc0a9f89a5154affbd12305ae008c34c7b32f00fe00127ab18d6bd", size = 148792, upload-time = "2025-12-16T00:48:12.324Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9f/ec51ebce455e17b6f304044e2bda57b15b1b45fd20b2feefa6e242fa33c6/livekit_blingfire-1.1.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:502d7a41fed246ec9cc432646d523c488a05fb2e572187a754735532ba5d69b7", size = 167606, upload-time = "2025-12-16T00:48:13.611Z" }, + { url = "https://files.pythonhosted.org/packages/d1/19/a4b56e54af456f2667287497f7678ff69a82ad21a687fc540213b4f25982/livekit_blingfire-1.1.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cdec36ea4d8b0dcda2791358ac9965e832539ecf13e651011197bab9960ea156", size = 174972, upload-time = "2025-12-16T00:48:14.811Z" }, + { url = "https://files.pythonhosted.org/packages/32/29/032cbf2c88ca40bee25b8a1b5346b5cb66487e689c4f42dd19f7e745090d/livekit_blingfire-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:28d8c822616ca2ce53125040dfe09d06a6cc3e63c9055d39ca767a5c8f67ef84", size = 131026, upload-time = "2025-12-16T00:48:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/81/50/46e410b935154a6bcf2d9494ee8e298b1a9c91ae33beaa78346703cf7681/livekit_blingfire-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5f6a40e498940f5b2e53d9753f5f7fb7f909e12a93a158844c9e3e99a5486b8", size = 154623, upload-time = "2025-12-16T00:48:17.641Z" }, + { url = "https://files.pythonhosted.org/packages/de/b4/f51c25bf104e51703dc66558ff9831a9769a9effa397956268902784a3d0/livekit_blingfire-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:945a672a224c9a686925e9af94c2660bacdbe190ccf693d6f17cea9359426c15", size = 148846, upload-time = "2025-12-16T00:48:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/ad95d195ed6dccb6527ed3c1e753f211c3e9509050af5cddf007608bb104/livekit_blingfire-1.1.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3aac3207cdd88c62323e0b07c33a69aac79c544122a2ddfbecc6c721ca760c", size = 167886, upload-time = "2025-12-16T00:48:19.858Z" }, + { url = "https://files.pythonhosted.org/packages/c5/67/fc4af1bbbed319d8edc319051bce720b51fa544f5d2ebb3201240779f135/livekit_blingfire-1.1.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:839feefa2910f99d794d3f3d696f95193ee8188cc6688a8d712bade2cede7951", size = 175858, upload-time = "2025-12-16T00:48:21.144Z" }, + { url = "https://files.pythonhosted.org/packages/76/6c/9e14763826476925767b511531318a83f95f3bf9e4dbc7dc611400af6e9e/livekit_blingfire-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:1409d4c297260b60a37bfe6ba21e4fb59dd53cd929632c0a78a28d41fe424302", size = 131048, upload-time = "2025-12-16T00:48:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9c/81fc3b7835fbee79ebf9a28cb4c673fa5fce30e5d8659fadaeed4ecb0f53/livekit_blingfire-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81a5942894944a5773dfa2ce800d016c5f5d8868cd01db1d4b099aa951df41aa", size = 154889, upload-time = "2025-12-16T00:48:23.181Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/976000dbd2781f5018cff52bc470cd48c7344ce5bd33f8417cbed5adaf8f/livekit_blingfire-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed861a19759987314ae71f2315d9bef11a3d6cc11e9effd5b9a3f0c567a3ba8d", size = 149170, upload-time = "2025-12-16T00:48:24.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/5f/f3b3b83758638aa90803946659dd6236253de3b529a9fb6148a0ad8dcab1/livekit_blingfire-1.1.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4cbcd68b2dea451be70e26cefa7cffb7a02aeacdbfa0efaa33cce7474e15983", size = 168075, upload-time = "2025-12-16T00:48:26.377Z" }, + { url = "https://files.pythonhosted.org/packages/ba/82/44e068acf6f9cf2abe0bc019c7073330cb8211e196e2767cacbdf3e4bf57/livekit_blingfire-1.1.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db593c044a3aff38af0b4d4f3d739aad4b24c740c80255bffaca07af2f9e6721", size = 175825, upload-time = "2025-12-16T00:48:27.377Z" }, + { url = "https://files.pythonhosted.org/packages/ad/2b/be8b3096727391726a7b382c5b98042ae6d29af1eee7c8467a93f823bcca/livekit_blingfire-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:bb23eb24d6a27df7205a562ce8c4d0a495d9d2a23aa3b5dec142d07401aeb342", size = 135843, upload-time = "2025-12-16T00:48:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9e/a9bf3a927c5c9544acef40a51f67b40aaba23bfcae63ab1f80418e25d9ce/livekit_blingfire-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6c4d3ee6ef7c0597dc737837997082d7a1eb69aed6efb12688dd2bef0d5b282", size = 157380, upload-time = "2025-12-16T00:48:29.268Z" }, + { url = "https://files.pythonhosted.org/packages/fa/61/8c2f68c4bab4202746c4f13670d8e7cf40dcbc0a32f292492e0c483f3811/livekit_blingfire-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5f4076098363dc5b57d8c5a6781a48539ebb35d82c1fe9587c047167ccf844d", size = 153248, upload-time = "2025-12-16T00:48:30.241Z" }, + { url = "https://files.pythonhosted.org/packages/72/08/93ed14e757f3bb3d22c40e11900ea2815883b7beb331fe758c8d40ea1dd0/livekit_blingfire-1.1.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20b95b2c15c4a1af4e68c1aa7d885bf295f89ee6eb5c1a1fdfa315a51795b30d", size = 170081, upload-time = "2025-12-16T00:48:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/b2a23cafabf55561c463490b3a87e323640259ae340e6db26bb71e0fa26d/livekit_blingfire-1.1.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c13ef26e5234674ac353b7cfaadeb23468e951eb07461b6b6c77f79cd5ff763", size = 176755, upload-time = "2025-12-16T00:48:32.294Z" }, + { url = "https://files.pythonhosted.org/packages/60/93/c00c175d2187160bdb2dac6b338203d51396307dfce23f03defb3b5e5572/livekit_blingfire-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91b2315e0497383384304d33554d70b8a63dec5ad96cd43437c67f4172077cf", size = 141072, upload-time = "2025-12-16T00:48:33.423Z" }, +] + +[[package]] +name = "livekit-local-inference" +version = "0.2.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/4d/4d6c184540c2b5fed02b7bd4c3d87c0f0add5127263af4f7f757382334f2/livekit_local_inference-0.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fedd281fd23b215fe36ce248445a3b0a4f0e1118c6ff7d0e0ad6a882281ecc1e", size = 34835670, upload-time = "2026-08-18T09:46:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/a7/831b0185f21ec1eb13b546c2e568fbd4951c806af77141e0f9e53add020f/livekit_local_inference-0.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bfda504af3cae4f2d9fe7abb25eed10238a5012c2c02fa7f53496f2382221020", size = 35093573, upload-time = "2026-08-18T09:46:11.089Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7d/1fdddd4b68220ae09b8a6145cc804b2653ad23593898c6246c4e5926c78c/livekit_local_inference-0.2.7-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e52bce657e9ded97489531c936a394d997271ec24867fbe0c72f2e0dc18465f2", size = 34832448, upload-time = "2026-08-18T09:46:13.845Z" }, + { url = "https://files.pythonhosted.org/packages/b6/20/988da55d6852c3e9082c0016bdda61f9c1a17192bc919afbcd68047bcdd0/livekit_local_inference-0.2.7-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cc08fdf6f22270bae68e680de8147a236c32356a6a3efcba05950bdbcb78bf0", size = 34874124, upload-time = "2026-08-18T09:46:17.273Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/127cf592f5fb3482b6f726ef2c792a93a27936bf22193293c639b8fc3abd/livekit_local_inference-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:5e13e3dde961febf72f059b247d1952d7114ed149309fe09b1cd88715b2c80a4", size = 34874533, upload-time = "2026-08-18T09:46:20.041Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c2/3131270c3e068a3a1605c13d382e5fe619643581b03194fcc9acec7b890f/livekit_local_inference-0.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ce67f4ad56df54fff3ea1c9d8966d33c825cf983c8b3c46593589fb434954abc", size = 34836817, upload-time = "2026-08-18T09:46:22.97Z" }, + { url = "https://files.pythonhosted.org/packages/18/a7/3bcce6ee79ad87abe521b10ce88c18d08939658fb53f28d6e92b4cc6e8be/livekit_local_inference-0.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa4cb2d495c80be6121642dba4def3a88b22a68d52c1b0affccd66c563de6622", size = 35094944, upload-time = "2026-08-18T09:46:25.88Z" }, + { url = "https://files.pythonhosted.org/packages/d5/47/18f484e8e6e4552bb74c1820e9b5bacdd2354975101f309a6bcd250e816d/livekit_local_inference-0.2.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de7753f545e93636d4e85fd485076d2bba2fc45bc43bf83c1ca492c2c7d6cfd1", size = 34833082, upload-time = "2026-08-18T09:46:28.513Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4b/4d552fa7acd6664158d69ba77d2d040a884030b13b36e8e552d6c5a062cb/livekit_local_inference-0.2.7-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b74561b380db6ac646d26f34b38bff97f8a2c461c8b7299f12a9643424dce73", size = 34873827, upload-time = "2026-08-18T09:46:31.077Z" }, + { url = "https://files.pythonhosted.org/packages/30/a5/d156e0168f672c1b5350859e6ce834b90c3f5819eb466d8cb2f58f890bbe/livekit_local_inference-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:205b147fc08e24f0b721b31c80b1d306fa00cc47eb23edd2db8d24f994b1bc21", size = 34875189, upload-time = "2026-08-18T09:46:33.69Z" }, + { url = "https://files.pythonhosted.org/packages/79/59/4a120c700508179d01af5525911acc044e2996d5fc86619896020b4fc41d/livekit_local_inference-0.2.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:86b64885254d554ca35483059e295bcf0281d7470b58301862e0367568633615", size = 34838988, upload-time = "2026-08-18T09:46:36.094Z" }, + { url = "https://files.pythonhosted.org/packages/ce/1a/0384f23dae195dca39fec32fb77fa5bf68bb022ca1648e9c1908b1228667/livekit_local_inference-0.2.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d66a509223e6e1f2ae77c52081969cafb49bd63d7bf4d50153f5bcdf52172865", size = 35096362, upload-time = "2026-08-18T09:46:38.893Z" }, + { url = "https://files.pythonhosted.org/packages/e0/29/18d7c360642e519124c8e5ae422dac36e22d74696a476edcc48fc6217d36/livekit_local_inference-0.2.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8fc1803a65654cc5061274a1aee067c584e2f0fd4ead85659f8f26ec06880f8", size = 34835065, upload-time = "2026-08-18T09:46:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/37/61/0b073e893274e461019516f8610029ceab74c06b9f6496b34be9ed704039/livekit_local_inference-0.2.7-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:449411e6cf01057e761588d6c19f1978d0ff652235d43de1780aa0b866b1435d", size = 34876716, upload-time = "2026-08-18T09:46:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/13/30/230770d450305c53cad880445d046c7f84c26507b61c50fe185ea15a29ca/livekit_local_inference-0.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:5a497ed9dc7f11666b2226cf195c85bd12f511ad7d57dbd7857bec4c573b9031", size = 34877098, upload-time = "2026-08-18T09:46:47.717Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ff/01233367f526c67df021d5ee5ad0e7d229553ad7e90d5ae02d5afbdc7abb/livekit_local_inference-0.2.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5b23b2fca99fbf05d349b8c1c1e499d9154997214db15b6784a999783f63169a", size = 34839008, upload-time = "2026-08-18T09:46:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/cc/45/9c70db9dc4581d9f2eecc04386bba3c8e74b6f438d2d6acb11aeaf66f96e/livekit_local_inference-0.2.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:866036cf42fce282404ecdad90bb2b814bc78aab245bc7be740e3cff528a36e8", size = 35096385, upload-time = "2026-08-18T09:46:53.359Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7d/0a981a4c7504fc4323a277d04705799fa48fbc036d368e47d5780c737341/livekit_local_inference-0.2.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f304cca187033b257cc8baf67f8799f5f467fa5f5984cdc3948591eb2027761b", size = 34835081, upload-time = "2026-08-18T09:46:55.883Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cc/858e2792eba28aa3baae1939488319f3bbef38c911cc0640f020bb0fe708/livekit_local_inference-0.2.7-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454c451a4df153f5a9c8c7ba20e842dd5c77993103fd1c03194856d351be87dd", size = 34876656, upload-time = "2026-08-18T09:46:58.567Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/b85d8f18fd46f335a559f6d39f8acf07663510d1add19d1f31814ef7daf0/livekit_local_inference-0.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:c16e86495346d8c349910ac8530de1e3532a6d1bac95ece0bc416c88f2a5c20f", size = 34877068, upload-time = "2026-08-18T09:47:01.317Z" }, + { url = "https://files.pythonhosted.org/packages/92/78/fd54ef156b91123f7bfe3c04d4163619275c9b5b802f9c084fbff49913fa/livekit_local_inference-0.2.7-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4a10f4e2322a4a4dae679be292dece7bb7a94ac55c27ab9b6d7ed2c5ff1ce68f", size = 34839377, upload-time = "2026-08-18T09:47:03.831Z" }, + { url = "https://files.pythonhosted.org/packages/77/f1/e1bef0798a102382afd9f041bcf16d96ea6e99ae4ad6956b292be7016cd5/livekit_local_inference-0.2.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dbb2c1d3b52d68914f6b469467cf1cdc56dcca06c7fb494995e692398f66d6e5", size = 35096694, upload-time = "2026-08-18T09:47:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/82/2e/7b4ec0de0e5f1ef4c2f441a3047dda950c11d819fb1804e0a13c6ef68317/livekit_local_inference-0.2.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f122e205899c5393ef4a710beedfece7a166649afa5ccbdc3cca61f2df113efa", size = 34835765, upload-time = "2026-08-18T09:47:08.974Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/5cee5031e9ac2b545f6a68d2442bb02a3a6564f298d91f321448c1b59944/livekit_local_inference-0.2.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a97bc2932b73f72c5246cb8350cf2cd7d21fc6237053fc7c1ac645352ed0f3f", size = 34876770, upload-time = "2026-08-18T09:47:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/c2c956d1797bd944449fd90945ceea5dded222a54b2df8c967c77440298f/livekit_local_inference-0.2.7-cp314-cp314-win_amd64.whl", hash = "sha256:168c2f2dd55ee6dd94fd7caae0ff73e4875e25aa418df819905fd339de1de4cd", size = 34882427, upload-time = "2026-08-18T09:47:13.983Z" }, +] + +[[package]] +name = "livekit-plugins-cartesia" +version = "1.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "livekit-agents" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/02/3fd0e466cbe553090ed8a87a6b4a288d9013bae44ef971b2a77f761f0d6d/livekit_plugins_cartesia-1.6.10.tar.gz", hash = "sha256:d23f7a248734c354a15519a0424b4d2f1c94b10becc29afccd763a8ec30c6402", size = 19474, upload-time = "2026-08-13T03:26:38.566Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/3d/53a8477f47d5f00329a27a38a1850c176d81ba4e41b22501eec88e277e75/livekit_plugins_cartesia-1.6.10-py3-none-any.whl", hash = "sha256:5e385b7dc02914f211bf9818d1d8e7582b8e2641baaf45017122433e68a32846", size = 27018, upload-time = "2026-08-13T03:26:37.356Z" }, +] + +[[package]] +name = "livekit-plugins-deepgram" +version = "1.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "livekit-agents", extra = ["codecs"] }, + { 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/ce/0b392068311a8b25dd4f544a64cc5bc54b2d13937b5d171488a4a2463a45/livekit_plugins_deepgram-1.6.10.tar.gz", hash = "sha256:6dbfbe9101c046e14304b620876100b0868690dba4fe92f6390496ee283f463d", size = 24673, upload-time = "2026-08-13T03:26:34.228Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/ef/dceb47df8743aa9b775c59d5a4a0172db21a686f68d8c71bf8d899c4e689/livekit_plugins_deepgram-1.6.10-py3-none-any.whl", hash = "sha256:a80fca3938db6141a8b0e1147b790edc30a3df65542d5a3b5637ba9dca3dbba9", size = 31783, upload-time = "2026-08-13T03:26:33.233Z" }, +] + +[[package]] +name = "livekit-plugins-elevenlabs" +version = "1.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "livekit-agents", extra = ["codecs"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/75/3cdab3916a8c398ae3607416b43a9fc80afe51975a91ae3e4fd1e79fd30a/livekit_plugins_elevenlabs-1.6.10.tar.gz", hash = "sha256:5a182100f0a57a6a8a8a75b4ccba2a9ed5fca2a11721e7220075bf6772aa6cf5", size = 19221, upload-time = "2026-08-13T03:26:46.072Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/00/3db7b6cbf5773b16dbc1f6730e3f6b5d703900ff40f8670d73fe8e43aee4/livekit_plugins_elevenlabs-1.6.10-py3-none-any.whl", hash = "sha256:eed9d041d84552b62ae65d13df44ad76a1fc67c79d432ef306cc60903ac57432", size = 21951, upload-time = "2026-08-13T03:26:44.936Z" }, +] + +[[package]] +name = "livekit-plugins-google" +version = "1.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "google-cloud-speech" }, + { name = "google-cloud-texttospeech" }, + { name = "google-genai" }, + { name = "livekit-agents" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/6b/317e8bb02068ea32ddb2fba575ae94ce30fe1ac758703ed84c4ddb0c6b35/livekit_plugins_google-1.6.10.tar.gz", hash = "sha256:df447a1c10dd9df606bc818087a58303f82bd4496a7eff974c32683d9dfdfdd3", size = 49703, upload-time = "2026-08-13T03:26:58.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/a9/d0d36826cf7c4d7286dc21a3f9bcf236692d806003603dd78f0de0eacc75/livekit_plugins_google-1.6.10-py3-none-any.whl", hash = "sha256:9bb58f4eacb3936a6412d234e27cef78c6a73673b25b2eefc2a93e87c3ac925d", size = 57568, upload-time = "2026-08-13T03:26:57.461Z" }, +] + +[[package]] +name = "livekit-plugins-openai" +version = "1.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "livekit-agents", extra = ["codecs", "images"] }, + { name = "openai", extra = ["realtime"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/d8/b0107ee2624b1b34e9970c398f228961c2368dd1293e356fccfeebb42955/livekit_plugins_openai-1.6.10.tar.gz", hash = "sha256:0022ebed58b444bad332389d7acbe79b8187221136243507fd6ef47afb3fdbb3", size = 54124, upload-time = "2026-08-13T03:28:02.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/54/df65469ed3bc80152e77db04bb87d7c570274937f92df1a5bde7eba3b5d1/livekit_plugins_openai-1.6.10-py3-none-any.whl", hash = "sha256:28adb7db24dc676964a42dea8d8dc0e2b7f11303ef2a4fe0c7e70af4007e0ded", size = 59905, upload-time = "2026-08-13T03:28:01.745Z" }, +] + +[[package]] +name = "livekit-plugins-silero" +version = "1.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "livekit-agents" }, + { 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", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "onnxruntime", version = "1.29.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/e8/5a2bde5122275e9f911abf02d178767802ef1d17618761965fce199736bf/livekit_plugins_silero-1.6.10.tar.gz", hash = "sha256:b24fb78ad16d125169ab78b23f7b40f229f78e43a936f2f591a43fdc5ed1d9ab", size = 1955427, upload-time = "2026-08-13T03:28:19.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/8c/90baf7db89dd20cf5731224d163d09a2a67752642ed3da51877c14ccf2f9/livekit_plugins_silero-1.6.10-py3-none-any.whl", hash = "sha256:f7e85b9f15f60a88844a71f011286a5081c7a7373ce30de83ead2d8eecc311ef", size = 3903154, upload-time = "2026-08-13T03:28:18.237Z" }, +] + +[[package]] +name = "livekit-protocol" +version = "1.1.22" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, + { name = "types-protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/65/736a378c2bf89c7fb54c1ff996f0bdc2046c588029d42afa2531b04c717d/livekit_protocol-1.1.22.tar.gz", hash = "sha256:a6517fd4ecea01ccd5055a30caefedc69a3e9ee02f715a79409b671091606692", size = 122570, upload-time = "2026-08-04T20:15:36.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/af/343dcfc7e429fbbc30993a733efb7fbfbbae70ac035a52e44111c6f4a76b/livekit_protocol-1.1.22-py3-none-any.whl", hash = "sha256:5c2edc843a48fe21d05b82c637c3e9eb92a88a34ba1e2c39857aca0e6105a84f", size = 149401, upload-time = "2026-08-04T20:15:35.073Z" }, +] + +[[package]] +name = "mako" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27", size = 410165, upload-time = "2026-08-05T06:10:56.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010, upload-time = "2026-08-05T06:10:58.248Z" }, +] + +[[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]] +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 = "mcp" +version = "1.29.0" +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/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, +] + +[[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 = "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 = "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 = "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'", +] +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 == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' 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.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' 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 = "onnxruntime" +version = "1.24.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c5/3af6b325f1492d691b23844d88ed26844c1164620860c5efe95c0e22782d/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978", size = 15130330, upload-time = "2026-03-05T16:34:53.831Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/f96b46c1866a293ed23ca2cf5e5a63d413ad3a951da60dd877e3c56cbbca/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d", size = 17213247, upload-time = "2026-03-05T17:17:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/36/13/27cf4d8df2578747584e8758aeb0b673b60274048510257f1f084b15e80e/onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39", size = 12595530, upload-time = "2026-03-05T17:18:49.356Z" }, + { url = "https://files.pythonhosted.org/packages/19/8c/6d9f31e6bae72a8079be12ed8ba36c4126a571fad38ded0a1b96f60f6896/onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723", size = 12261715, upload-time = "2026-03-05T17:18:39.699Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/dfdc4e52600fde4c02d59bfe98c4b057931c1114b701e175aee311a9bc11/onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91", size = 17342578, upload-time = "2026-03-05T17:19:02.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/dc/1f5489f7b21817d4ad352bf7a92a252bd5b438bcbaa7ad20ea50814edc79/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452", size = 15150105, upload-time = "2026-03-05T16:34:56.897Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/fd253da53594ab8efbefdc85b3638620ab1a6aab6eb7028a513c853559ce/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d", size = 17237101, upload-time = "2026-03-05T17:18:02.561Z" }, + { url = "https://files.pythonhosted.org/packages/71/5f/eaabc5699eeed6a9188c5c055ac1948ae50138697a0428d562ac970d7db5/onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a", size = 12597638, upload-time = "2026-03-05T17:18:52.141Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/d8066c320b90610dbeb489a483b132c3b3879b2f93f949fb5d30cfa9b119/onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8", size = 12270943, upload-time = "2026-03-05T17:18:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/51/8d/487ece554119e2991242d4de55de7019ac6e47ee8dfafa69fcf41d37f8ed/onnxruntime-1.24.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:34a0ea5ff191d8420d9c1332355644148b1bf1a0d10c411af890a63a9f662aa7", size = 17342706, upload-time = "2026-03-05T16:35:10.813Z" }, + { url = "https://files.pythonhosted.org/packages/dd/25/8b444f463c1ac6106b889f6235c84f01eec001eaf689c3eff8c69cf48fae/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fd2ec7bb0fabe42f55e8337cfc9b1969d0d14622711aac73d69b4bd5abb5ed7", size = 15149956, upload-time = "2026-03-05T16:34:59.264Z" }, + { url = "https://files.pythonhosted.org/packages/34/fc/c9182a3e1ab46940dd4f30e61071f59eee8804c1f641f37ce6e173633fb6/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df8e70e732fe26346faaeec9147fa38bef35d232d2495d27e93dd221a2d473a9", size = 17237370, upload-time = "2026-03-05T17:18:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/3b549e1f4538514118bff98a1bcd6481dd9a17067f8c9af77151621c9a5c/onnxruntime-1.24.3-cp313-cp313-win_amd64.whl", hash = "sha256:2d3706719be6ad41d38a2250998b1d87758a20f6ea4546962e21dc79f1f1fd2b", size = 12597939, upload-time = "2026-03-05T17:18:54.772Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/9696a5c4631a0caa75cc8bc4efd30938fd483694aa614898d087c3ee6d29/onnxruntime-1.24.3-cp313-cp313-win_arm64.whl", hash = "sha256:b082f3ba9519f0a1a1e754556bc7e635c7526ef81b98b3f78da4455d25f0437b", size = 12270705, upload-time = "2026-03-05T17:18:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/b7/65/a26c5e59e3b210852ee04248cf8843c81fe7d40d94cf95343b66efe7eec9/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f956634bc2e4bd2e8b006bef111849bd42c42dea37bd0a4c728404fdaf4d34", size = 15161796, upload-time = "2026-03-05T16:35:02.871Z" }, + { url = "https://files.pythonhosted.org/packages/f3/25/2035b4aa2ccb5be6acf139397731ec507c5f09e199ab39d3262b22ffa1ac/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d1f25eed4ab9959db70a626ed50ee24cf497e60774f59f1207ac8556399c4d", size = 17240936, upload-time = "2026-03-05T17:18:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/b3240ea84b92a3efb83d49cc16c04a17ade1ab47a6a95c4866d15bf0ac35/onnxruntime-1.24.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a6b4bce87d96f78f0a9bf5cefab3303ae95d558c5bfea53d0bf7f9ea207880a8", size = 17344149, upload-time = "2026-03-05T16:35:13.382Z" }, + { url = "https://files.pythonhosted.org/packages/bb/4a/4b56757e51a56265e8c56764d9c36d7b435045e05e3b8a38bedfc5aedba3/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d48f36c87b25ab3b2b4c88826c96cf1399a5631e3c2c03cc27d6a1e5d6b18eb4", size = 15151571, upload-time = "2026-03-05T16:35:05.679Z" }, + { url = "https://files.pythonhosted.org/packages/cf/14/c6fb84980cec8f682a523fcac7c2bdd6b311e7f342c61ce48d3a9cb87fc6/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e104d33a409bf6e3f30f0e8198ec2aaf8d445b8395490a80f6e6ad56da98e400", size = 17238951, upload-time = "2026-03-05T17:18:12.394Z" }, + { url = "https://files.pythonhosted.org/packages/57/14/447e1400165aca8caf35dabd46540eb943c92f3065927bb4d9bcbc91e221/onnxruntime-1.24.3-cp314-cp314-win_amd64.whl", hash = "sha256:e785d73fbd17421c2513b0bb09eb25d88fa22c8c10c3f5d6060589efa5537c5b", size = 12903820, upload-time = "2026-03-05T17:18:57.123Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/6b2fa5702e4bbba7339ca5787a9d056fc564a16079f8833cc6ba4798da1c/onnxruntime-1.24.3-cp314-cp314-win_arm64.whl", hash = "sha256:951e897a275f897a05ffbcaa615d98777882decaeb80c9216c68cdc62f849f53", size = 12594089, upload-time = "2026-03-05T17:18:47.169Z" }, + { url = "https://files.pythonhosted.org/packages/12/dc/cd06cba3ddad92ceb17b914a8e8d49836c79e38936e26bde6e368b62c1fe/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d4e70ce578aa214c74c7a7a9226bc8e229814db4a5b2d097333b81279ecde36", size = 15162789, upload-time = "2026-03-05T16:35:08.282Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/413e98ab666c6fb9e8be7d1c6eb3bd403b0bea1b8d42db066dab98c7df07/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02aaf6ddfa784523b6873b4176a79d508e599efe12ab0ea1a3a6e7314408b7aa", size = 17240738, upload-time = "2026-03-05T17:18:15.203Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/a8/0520890321b8ff40b908cf165a93eb58fbc8f85c14db637277ea866c9544/onnxruntime-1.29.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:07c5907474dec4a2792fd7626b753dc66707808385a6d9eecf993db0066a9d0f", size = 21420890, upload-time = "2026-08-17T22:53:33.429Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/8bd3e0008ff8d386305351109a7329ea57e51a3ab57bc92340f29c4a5b5d/onnxruntime-1.29.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:16925ef8497e2c07e4b5ae15b504079b3ab3f65e22c58efd10dde0f3caea969a", size = 20803602, upload-time = "2026-08-17T22:53:36.47Z" }, + { url = "https://files.pythonhosted.org/packages/3b/91/a66cd77f28379ede419672edda3184f1eb286db215dce1e7b976fae2d63b/onnxruntime-1.29.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:85f8e8406c52658735fe5c7fbfd3ebaa1ed340768324f6252e4274e374580a23", size = 23113193, upload-time = "2026-08-17T22:53:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/1c/82/2da968405c42340f03de0bcdb63be09ae1004f820b2295590d48951b5cf2/onnxruntime-1.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:0d4f427afac434b0070fe992b540ddf20a7aff2265f760f314d91331935b6b98", size = 13999253, upload-time = "2026-08-17T22:53:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/95/7a/70c9c893bf732ee66124c2d8de6a21fc9361ec62cf378f857043efcbf0eb/onnxruntime-1.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:4eae472cf7dc3107dec1bb53cd6d142d1964616d08aae48654cd4254b2363c4b", size = 13741410, upload-time = "2026-08-17T22:53:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/d4/80/381c1e9efed9cc32d00aa7cab0547dc84116cec906c3ffe3613686d6963a/onnxruntime-1.29.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3a3814c041251d6a77fdf513fb282056538ee826d2f1178a0df3c549d3fff6ba", size = 21430049, upload-time = "2026-08-17T22:53:48.286Z" }, + { url = "https://files.pythonhosted.org/packages/30/12/4be0e345d38fe707a701ca07e8f63c05b152a2e6285d1e43a7faf63fedd2/onnxruntime-1.29.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d2fb19e848f7c33ed8d3182b52504aaa11c5e8da438bbb47296f85b133cbcf6b", size = 20816870, upload-time = "2026-08-17T22:53:51.169Z" }, + { url = "https://files.pythonhosted.org/packages/96/eb/e6968f5e41aac3125f2ff5708855f09cb0b70d85ed3115b625b0b58305ba/onnxruntime-1.29.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2b80d8c7ec2cc7438e4da3760b88c24568cba72c9ace96d668800a6c79419acb", size = 23136745, upload-time = "2026-08-17T22:53:53.92Z" }, + { url = "https://files.pythonhosted.org/packages/b4/80/5b28f1f1111210fc4a336ddbc6950f468ebf9a6a265420568f4f43fa33ce/onnxruntime-1.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:4acf2b4948b7ede87221ca6332344b8facdc8059d6ac751a7d367d04532b02dd", size = 14001407, upload-time = "2026-08-17T22:53:56.486Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d6/6883f89ea4b044e6e8447ebfaf9bcecdf457b7d80a683635e130b25498e0/onnxruntime-1.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:dc61a79cb39afd66ab3f01fd2c23591a7f01de89c1668e1fb6315067fc279164", size = 13746981, upload-time = "2026-08-17T22:53:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/41/f8/d375facf60edaf41f5732f9f689c98a800fcc52df5cf6ddfb406703eb5a1/onnxruntime-1.29.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:be0f8ed688cfb1d4d5765a137193b7bfab0c8ea214eed99260b380bb525a3a7f", size = 21429708, upload-time = "2026-08-17T22:54:01.44Z" }, + { url = "https://files.pythonhosted.org/packages/c9/17/b9ad04051a8c4f504852ce0e8e10f9a6b2f1a331eedcdcc503df776dd0ea/onnxruntime-1.29.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:d67673c5367727860922c5262d724472f1b5539fb7ccf4c81a638f9b71719803", size = 20816263, upload-time = "2026-08-17T22:54:04.088Z" }, + { url = "https://files.pythonhosted.org/packages/83/2c/d8eb945d2a372149df9705a8d5c8d7c6c46c987c5446dbcea9e1ea7f6556/onnxruntime-1.29.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e2128f31f449e922c62dbe5d8b6b7b079f0bcaf2d56a102fa203cb6e5bb5ab19", size = 23136817, upload-time = "2026-08-17T22:54:06.714Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3b/66b424c63fa92dfaa48d1719efaae66fc8c256b9426a832eda51d8dfe1e9/onnxruntime-1.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:2945e1f82f81f27e88decea88c7861f45baea23818950d467bf3909aa303119e", size = 14001310, upload-time = "2026-08-17T22:54:09.13Z" }, + { url = "https://files.pythonhosted.org/packages/83/22/d6a700e3a6322fa3d56fbe7cee9ffc53f35e77ffcd6b7e97f4b7722a27ab/onnxruntime-1.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:4b940b0d777590c7e20bf298f5c16af1ea6ad1b400a1c822a6be192f64f4d954", size = 13747112, upload-time = "2026-08-17T22:54:11.608Z" }, + { url = "https://files.pythonhosted.org/packages/4a/89/c4af146de3d60a32c89fea48d5d34bfd044faaf8957270043a03bd1b462b/onnxruntime-1.29.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:533f8370ce124304e5cb08ab961836cf755631e3dd77adc5f3bbdab70c2b7d99", size = 20826136, upload-time = "2026-08-17T22:54:14.315Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/e6bbacd11dfe8d070613261a758795ea128b9fc9bea391a2a7da2e4c7a08/onnxruntime-1.29.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c1ad3f437153fe77f9d01a08fbaac0beb030e09b8a80ace1603bcf69b6c95481", size = 23138951, upload-time = "2026-08-17T22:54:17.154Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a3/718e1b83096a1bc7b0fc8014c23d4cf795559fe666961cfac4fc038a4871/onnxruntime-1.29.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e74b278af1d949876f5d91d1268fd6c680e79f2bac194967394eaba9fdf69e7e", size = 21431104, upload-time = "2026-08-17T22:54:20.118Z" }, + { url = "https://files.pythonhosted.org/packages/4e/17/c75e78ddc1fe69b6ebaef7fe88ac83f29bfe10955e3a0d2436d93473c91c/onnxruntime-1.29.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:939e5d65f332e6d399774b2bd0d3559fd8fa629c1e77833db29d968d2384f23d", size = 20818488, upload-time = "2026-08-17T22:54:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/54/9f197c578d3d3d7bea16971e233e5483981228eec73748585cf7b5933403/onnxruntime-1.29.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6c0c37b92f67ed68dd36221ce0403e1d9bd4f7efce724439978a2597848530e5", size = 23136994, upload-time = "2026-08-17T22:54:26.321Z" }, + { url = "https://files.pythonhosted.org/packages/24/53/4616a55d2495679cfd0195f968feb3d74fe30e26467d168ee243ac97c089/onnxruntime-1.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:4a3129ae56e70d2618ff773920166916310370a7e3cacb60b9e0e8910092725f", size = 14350643, upload-time = "2026-08-17T22:54:28.794Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0f/c338cb5500a522c7e671a3bb1276f4562404fbecce8a0e274565aa968484/onnxruntime-1.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:e417ef8628dcce310d2d53023e750ea298ec14d4341ae6dc3a572bfd9bc7fa97", size = 14124294, upload-time = "2026-08-17T22:54:31.015Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e7/61064289a9a1301b25c1f0f574fe98aba31c2d388db3c1dbec664f78621f/onnxruntime-1.29.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:11264bb58f7b7cf6af835ab10d36838d73680580820fd6f51d90124a1ca8f449", size = 20826174, upload-time = "2026-08-17T22:54:34.283Z" }, + { url = "https://files.pythonhosted.org/packages/60/21/d0c04b561b46e9bff89b5f500fb7415b8ca0669f7902204f76ab06bb0c7e/onnxruntime-1.29.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1ea91cef3b971506e51ae9c37c16d027774ec64994a524ec1bdfb027d68a9832", size = 23138547, upload-time = "2026-08-17T22:54:37.491Z" }, +] + +[[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.optional-dependencies] +realtime = [ + { name = "websockets" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/45/7af37fe54e5d3e66e7dcd7ba8b8aeee73f202bfac909cc94b8c4e428f9ac/opentelemetry_exporter_otlp-1.44.0.tar.gz", hash = "sha256:af1cde7c33ea8ed624bf04ac49a885730fe44c1f1ad698656e592c38f70ce106", size = 6090, upload-time = "2026-07-16T15:25:34.585Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/c3/7b466a9463944e70b37b744072a0c1b88a425dade3fff0631adec66c9bcc/opentelemetry_exporter_otlp-1.44.0-py3-none-any.whl", hash = "sha256:4a498fa8d8fd8be9e8e2d175fe5524a3fe581ccffadd8509db86526a5fb97051", size = 6727, upload-time = "2026-07-16T15:25:14.445Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.44.0" +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/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.44.0" +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/1a/87/95e2a5aaa795b4e2260d74e16df2d5541deb2ea9de010bcd615f4dee2654/opentelemetry_exporter_otlp_proto_http-1.44.0.tar.gz", hash = "sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8", size = 25806, upload-time = "2026-07-16T15:25:39.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/d0/fdeb1a98d8d3a6205f5f297c51b4a9bfe65126ab60339669bbe3dd54c2e2/opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3", size = 21850, upload-time = "2026-07-16T15:25:20.006Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, +] + +[[package]] +name = "optuna" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "colorlog" }, + { 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 = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/aa/05f5e3f662cc96a4c478fc3446b8ed6359825a2b504ecb614a9ac84e4a4d/optuna-4.9.0.tar.gz", hash = "sha256:b322e5cbdf1655fb84c37646c4a7a1f391de1b47806bbe222e015825d0a82b87", size = 485834, upload-time = "2026-06-01T06:23:30.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/f3/e5fcd5d9b15771ed6dc10e3a7eeddc672e418f4f4c4653d216cc1d857e2d/optuna-4.9.0-py3-none-any.whl", hash = "sha256:f52f3be6148654850c92a5860d398fd88ec6b2c84ab68d9c3d07dcff02e7afee", size = 425553, upload-time = "2026-06-01T06:23:28.804Z" }, +] + +[[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 = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, + { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, + { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, +] + +[[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 = "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 = "prometheus-client" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, +] + +[[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 = "proto-plus" +version = "1.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/6a/056256feb4bd000869aba5c16cf2aa911572ca2a2feb185f86e457b5171e/proto_plus-1.28.3.tar.gz", hash = "sha256:5f91b30dafa6bb38d432c5557a6ee1d35ffd40b4b1e0e3ca27260448560b91d9", size = 58051, upload-time = "2026-08-06T06:24:55.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/3a/cfee3c50294f55a2f0f9575052dec2c2a48891ad4b1c2a133b05a87026cd/proto_plus-1.28.3-py3-none-any.whl", hash = "sha256:dc76880b8ee951cca002098574376cf71e055f9f16d9ba6570fb8a06f726d281", size = 50795, upload-time = "2026-08-06T06:23:50.653Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[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.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "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 = "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 = "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 = "pytz" +version = "2026.3.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, +] + +[[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 = "rapidfuzz" +version = "3.14.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/21/ef6157213316e85790041254259907eb722e00b03480256c0545d98acd33/rapidfuzz-3.14.5.tar.gz", hash = "sha256:ba10ac57884ce82112f7ed910b67e7fb6072d8ef2c06e30dc63c0f604a112e0e", size = 57901753, upload-time = "2026-04-07T11:16:31.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/b1/d6d6e7737fe3d0eb2ac2ac337686420d538f83f28495acc3cc32201c0dbf/rapidfuzz-3.14.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:071d96b957a33b9296b9284b6350a0fb6d030b154a04efd7c15e56b98b79a517", size = 1953508, upload-time = "2026-04-07T11:13:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7b/94c1c953ac818bdd88b43213a9d38e4a41e953b786af3c3b2444d4a8f96d/rapidfuzz-3.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667f40fe9c81ad129b198d236881b00dd9e8314d9cc72d03c3e16bdfe5879051", size = 1160895, upload-time = "2026-04-07T11:13:39.278Z" }, + { url = "https://files.pythonhosted.org/packages/7f/60/a67a7ca7c2532c6c1a4b5cd797917780eed43798b82c98b6df734a086c95/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9fff308486bbd2c8c24f25e8e152c7594d3fe8db265a2d6a1ce24d58671127f", size = 1382245, upload-time = "2026-04-07T11:13:41.054Z" }, + { url = "https://files.pythonhosted.org/packages/95/ff/a42c9ce9f9e90ceb5b51136e0b8e8e6e5113ba0b45d986effbd671e7dddf/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dfa552338f51aec280f17b02d28bace1e162d1a84ccd80e3339a57f98aedb56b", size = 3163974, upload-time = "2026-04-07T11:13:42.662Z" }, + { url = "https://files.pythonhosted.org/packages/e3/3c/11e2d41075e6e48b7dad373631b379b7e40491f71d5412c5a98d3c58f60f/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:068b3e965ca9d9ee4debe40001ae7c3938ba646308afd33cf0c66618147db65c", size = 1475540, upload-time = "2026-04-07T11:13:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/29/fa/09be143dcc22c79f09cf90168a574725dbda49f02cbbd55d0447da8bec86/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88b7d31ff1cc5e9bc0e4406e6b1fa00b6d37163d50bb58091e9b976ff1129faa", size = 2404128, upload-time = "2026-04-07T11:13:46.641Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/1aeb504cdcfde42881825e9c86f48238d4e01ba8a1530491e82eb17e5689/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eacb434410b8d9ca99a8d42352ef085cf423e3c76c1f0b86be2fcba3bff2952c", size = 2508455, upload-time = "2026-04-07T11:13:48.726Z" }, + { url = "https://files.pythonhosted.org/packages/10/8e/b1b5eed8d887a29b0e18fd3222c46ca60fddfb528e7e1c41267ce42d5522/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:649712823f3abcdc48427147a5384fac15623ba435d0013959b52e6462521397", size = 4274060, upload-time = "2026-04-07T11:13:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/7e5b0353693d4f47b8b0f96e941efc377cfb2034b67ef92d082ac4441a0f/rapidfuzz-3.14.5-cp310-cp310-win32.whl", hash = "sha256:13cb79c23ef5516e4c4e3830877be8b19aa75203636be1163d690d37803f6504", size = 1727457, upload-time = "2026-04-07T11:13:52.45Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6e/f530a39b946fa71c009bc9c81fdb6b48a77bbc57ee8572ac0302b3bf6308/rapidfuzz-3.14.5-cp310-cp310-win_amd64.whl", hash = "sha256:f2073495a7f9b75e57e600747ac09510d67683fd64d3228e009740b7ef88f9fe", size = 1544657, upload-time = "2026-04-07T11:13:54.952Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/02fa075f9f59ff766d374fecbd042b3ac9782dcd5abc52d909a54f587eeb/rapidfuzz-3.14.5-cp310-cp310-win_arm64.whl", hash = "sha256:8166efddea49fdbc61185559f47593239e4794fd7c9044dd5a789d1a90af852d", size = 816587, upload-time = "2026-04-07T11:13:56.418Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f9/3c41a7be8855803f4f6c713b472226a98d31d41869d98f64f4ca790510d6/rapidfuzz-3.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e251126d48615e1f02b4a178f2cd0cd4f0332b8a019c01a2e10480f7552554b4", size = 1952372, upload-time = "2026-04-07T11:13:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/9e/89/c2557e37531d03465193bff0ab9de70b468420a807d71a26a65100635459/rapidfuzz-3.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ab449c9abd0d4e1f8145dce0798a4c822a1a1933d613c764a641bea88b8bdab", size = 1159782, upload-time = "2026-04-07T11:14:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b2/ffeeb7eca1a897d51b998f4c0ef0281696c3b06abcca4f88f9def708ffe1/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb2829fedd672dd7107267189dabe2bbe07972801d636014417c6861eb89e358", size = 1383677, upload-time = "2026-04-07T11:14:01.696Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d0/4539e42a2d596e068f7738f279638a4a74edd1fbb6f8594e2458058979c6/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d50e5861872935fece391351cbb5ba21d1bced277cf5e1143d207a0a35f1925", size = 3168906, upload-time = "2026-04-07T11:14:03.29Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1c/3ec897eb9d8b05308aa8ef6ae4ed64b088ad521a3f9d8ff469e7e97bc2b0/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:7092a216728f80c960bd6b3807275d1ee318b168986bd5dc523349581d4890b8", size = 1478176, upload-time = "2026-04-07T11:14:04.94Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ba/970c03a12ce20a5399e22afe9f8932fd4cd1265b8a8461d0e63b00eb4eae/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9669753caef7fdc6529f6adcc5883ed98d65976445d9322e7dbdb6b697feee13", size = 2402441, upload-time = "2026-04-07T11:14:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/81/93/61d351cae60c1d0e21ba5ff1a1015ad045539ed215da9d6e302204ed887a/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:823b1b9d9230809d8edcc18872770764bfe8ef4357995e16744047c8ccf0e489", size = 2511628, upload-time = "2026-04-07T11:14:09.234Z" }, + { url = "https://files.pythonhosted.org/packages/87/52/374d2d4f60fd98155142a869323aa221e30868cfa1f15171a0f64070c247/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f0b2af76b7e7060c09e1a0dfa9410eb19369cbe6164509bff2ef94094b54d2b6", size = 4275480, upload-time = "2026-04-07T11:14:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/d8/04/82e7989bc9ec20a15b720a335c5cb6b0724bf6582013898f90a3280cfccd/rapidfuzz-3.14.5-cp311-cp311-win32.whl", hash = "sha256:c5801a89604c65ab4cc9e91b23bc4076d0ca80efd8c976fb63843d7879a85d7f", size = 1725627, upload-time = "2026-04-07T11:14:13.217Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b5/eca8ac5609bc9bcb02bb6ff87fa5983cc92b8772d66a431556ab8a8c178f/rapidfuzz-3.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:d7ca16637c0ede8243f84074044bd0b2335a0341421f8227c85756de2d18c819", size = 1545977, upload-time = "2026-04-07T11:14:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e1/dbf318de28f65fa2cdd0a9dfbdee380f8199eb83b19259bc4f8592551b4e/rapidfuzz-3.14.5-cp311-cp311-win_arm64.whl", hash = "sha256:8c90cdf8516d9057e502aa6003cea71cf5ec27cc44699ca52412b502a04761bb", size = 816827, upload-time = "2026-04-07T11:14:16.788Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e3/574435c6aafb80254c191ef40d7aca2cb2bb97a095ec9395e9fa59ac307a/rapidfuzz-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d3378f471ef440473a396ce2f8e97ee12f89a78b495540e0a5617bbfe895638", size = 1944601, upload-time = "2026-04-07T11:14:18.771Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1f/fbad3102a255ecc112ce9a7e779bacab7fd14398217be8868dc9082ba363/rapidfuzz-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e910eebca9fd0eba245c0555e764597e8a0cccb673a92da2dc2397050725f48", size = 1164293, upload-time = "2026-04-07T11:14:20.534Z" }, + { url = "https://files.pythonhosted.org/packages/88/37/a3eb7ff6121ed3a5f199a8c38cc86c8e481816f879cb0e0b738b078c9a7e/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01550fe5f60fd176aa66b7611289d46dc4aa4b1b904874c7b6d1d54e581c5ec1", size = 1371999, upload-time = "2026-04-07T11:14:22.63Z" }, + { url = "https://files.pythonhosted.org/packages/79/72/97a9728c711c7c1b06e107d3f0623880fb4ef90e147ed13c551a1730e7cc/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48bee0b91bebfaec41e1081e351000659ab7570cc4598d617aa04d5bf827f9e6", size = 3145715, upload-time = "2026-04-07T11:14:24.508Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/d5caabbea233ac90c286c87c260e49d7641467e87438a18d858e41c82e91/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:7e580cb04ad849ae9b786fa21383c6b994b6e6c1444ad1cb9f22392759d72741", size = 1456304, upload-time = "2026-04-07T11:14:26.515Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a7/2d1a81250ac8c01a0100c026018e76f0e7a097ff63e4c553e02a6938c6fb/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:09d6c9ba091854f07817055d795d604179c12a8f308ba4c7d56f3719dfea1646", size = 2389089, upload-time = "2026-04-07T11:14:28.635Z" }, + { url = "https://files.pythonhosted.org/packages/65/0d/c47c3872203ae88e6506997c0b576ad731f5261daa25d559be09c9756658/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1e989f86113be66574113b9c7bdf4793f3f863d248e47d911b355e05ca6b6b10", size = 2493404, upload-time = "2026-04-07T11:14:30.577Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2f/71e0a5a3130792146c8a200a2dd1e52aa16f7c1074012e17f2601eea9a90/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ebd1a18e2e47bc0b292a07e6ed9c3642f8aaa672d12253885f599b50807a4f9", size = 4251709, upload-time = "2026-04-07T11:14:32.451Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/d39874901abacef325adb5b34ae416817c8486dfb4fb87c7a9b74ec5b072/rapidfuzz-3.14.5-cp312-cp312-win32.whl", hash = "sha256:9981d38a703b86f0e315a3cd229fd1906fe1d91c989ed121fb975b3c849f89f5", size = 1710069, upload-time = "2026-04-07T11:14:34.37Z" }, + { url = "https://files.pythonhosted.org/packages/85/0b/f65572c53de8a1c704bda707f63a447b67bdbe95d7cdc70d18885e191df5/rapidfuzz-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:d8375e3da319593389727c3187ccaf3e0e84199accc530866b8e0f2b79af05e9", size = 1540630, upload-time = "2026-04-07T11:14:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c3/143be3a578f989758cae516f3270d5cbb49783a7bfdf57cc27a670e00456/rapidfuzz-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:478b59bb018a6780d73f33e38d0b3ec5e968a6c1ed42876b993dd456b7aa20e8", size = 813137, upload-time = "2026-04-07T11:14:38.289Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/252803f2010ba699618cdc048b6e1f7cc1f433c08b4a9a17579b92ab0142/rapidfuzz-3.14.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd8fd343bf8492a1e60bcb6dc99f90f74f65d98d8241a6b3e1fed225b76ecd6", size = 1940205, upload-time = "2026-04-07T11:14:40.319Z" }, + { url = "https://files.pythonhosted.org/packages/ea/59/b2afd98e41af9cd54554a4c1c423d84cdd60e6b1c0a09496f033b55f60ec/rapidfuzz-3.14.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6737b35d5af7479c5bf9710f7b17edd9d2c43128d974d25fb4ea653e42c64609", size = 1159639, upload-time = "2026-04-07T11:14:42.52Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/7aa7e62c4c516a7af322ed0c4f0774208b72d457d0cfec808bad0df12f4a/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b002c7994cc9f2bc9d9856f0fbaee6e8072c983873846c92f25cefba5b2a925f", size = 1367194, upload-time = "2026-04-07T11:14:44.25Z" }, + { url = "https://files.pythonhosted.org/packages/90/79/2fc252a63bc91d3c3b234d0a3a6ad4ebc460037a23cdcdaf9285f986e6c9/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17a34330cd2a538c1ce5d400b61ba358c5b72c654b928ff87b362e88f8b864c7", size = 3151805, upload-time = "2026-04-07T11:14:46.21Z" }, + { url = "https://files.pythonhosted.org/packages/17/54/0c83508f2683ea70e2d05f8527eb07328acf7bb1e9d97a3bece5702378e7/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:95d937e74c1a7a1287dfb03b62a827be08ede10a155cf1af73bbf47f2b73ee6e", size = 1455667, upload-time = "2026-04-07T11:14:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/71/1b/070175e873177814d58850a01ebe80e20ae11e93eb4da894d563988660fa/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:46b92a9970dcc34f0096901c792644094cab49554ac3547f35e3aebbdf0a3610", size = 2388246, upload-time = "2026-04-07T11:14:50.098Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/77caf7aaf9c2be050ad1f128d7c24ff0f59079aa62c5f62f9df41c0af45e/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e012177c8e8a8a0754ae0d6027d63042aa5ff036d9f40f07cb3466a6082e21b8", size = 2494333, upload-time = "2026-04-07T11:14:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/dd7e1f2aa31a8fbbfc16b0610af1d770ffaf1287490f3c8c5b1c52da264f/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ae6f53f99c9a0eca7a0afc5b4e45fc73bc1dd4ac74c00509031d76df80ed98", size = 4258579, upload-time = "2026-04-07T11:14:54.538Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/ac99e1ba347ba0e85e0bb60b74231d55fb93c0eff43f2920ccb413d0be08/rapidfuzz-3.14.5-cp313-cp313-win32.whl", hash = "sha256:4a60f0057231188e3bd30216f7b4e0f279b11fa4ec818bb6c1d9f014d1562fbc", size = 1709231, upload-time = "2026-04-07T11:14:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cb/0e251d731b3166378644238e8f0cf9e89858c024e19f75ca9f7e3ae83fd5/rapidfuzz-3.14.5-cp313-cp313-win_amd64.whl", hash = "sha256:11bfc2ed8fbe4ab86bd516fadefab126f90e6dcadffa761739fcb304707dfd35", size = 1538519, upload-time = "2026-04-07T11:14:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/4548132acc947db6d5346a248e44a8b3a22d608ef30e770fb578caaf2d00/rapidfuzz-3.14.5-cp313-cp313-win_arm64.whl", hash = "sha256:b486b5218808f6f4dc471b114b1054e63553db69705c97da0271f47bd706aedd", size = 812628, upload-time = "2026-04-07T11:15:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/00/60/69b177577290c5eab892c6f75fe89c3aff3f9ae80298a78d9372b1cecb9a/rapidfuzz-3.14.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39ef8658aaf67d51667e7bdaf7096f432333377d8302ac43c70b5df8a4cf89b8", size = 1970231, upload-time = "2026-04-07T11:15:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/48/38/2fd790052659cc4e2907b63c25433f0987864b445c1aeec1a302ef5ad948/rapidfuzz-3.14.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ad37a0be705b544af6296da8edddc260d10a8ae5462530fc9991f66498bb1f9", size = 1194394, upload-time = "2026-04-07T11:15:04.572Z" }, + { url = "https://files.pythonhosted.org/packages/80/f4/28430ad8472fc3536e8ebd51a864a226e979cfe924c6e3f83d111373aa74/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d45e06f60729e07d9b20c205f7e5cff90b6ef2584e852eecf46e045aea69627d", size = 1377051, upload-time = "2026-04-07T11:15:06.728Z" }, + { url = "https://files.pythonhosted.org/packages/77/7e/9aeacabcfd1e77397968362e5b98fe14248b8307011136b17daf99752a8e/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e52da10236aa6212de71b9e170bace65b64b129c0dea7fc243d6c9ce976f5074", size = 3160565, upload-time = "2026-04-07T11:15:08.667Z" }, + { url = "https://files.pythonhosted.org/packages/56/f4/db4dd7be0cd2f2022117ac5407d905f435d60e48baaea313a567ad27e865/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:440d30faaf682ca496170a7f0cc5453ec942e3e079f0fd802c9a7f938dfb50a3", size = 1442113, upload-time = "2026-04-07T11:15:11.138Z" }, + { url = "https://files.pythonhosted.org/packages/a4/99/0e9f6aa57f3e32a767216f797e56dc96b720fcecfb9d8ee907ecc82f8d66/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56227a61fd3d17b0cd9793132431f3a3d07c8654be96794ba9f89fe0fc8b2d09", size = 2396618, upload-time = "2026-04-07T11:15:13.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/94/44a78e39ffce17cbdd3e2b53b696acc751d5d153be0f499d052b07a4d904/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:2e83cd2e25bb4edd97b689d9979d9c3acccdaaf26ceac08212ceece202febcfa", size = 2478220, upload-time = "2026-04-07T11:15:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/454311469a09a507e9d784a35796742bec22e4cebe75551e2da4e0e290fd/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:af3b859726cd3374287e405e14b9634563c078c5531a4f62375508addebddad1", size = 4265027, upload-time = "2026-04-07T11:15:17.28Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/175465a9ab3e3b70ba669058372f009d1d49c1746e2dcd56b69df188d3a5/rapidfuzz-3.14.5-cp313-cp313t-win32.whl", hash = "sha256:8ce1d850b3c0178440efde9e884d98421b5e87ff925f364d6d79e23910d7593f", size = 1766814, upload-time = "2026-04-07T11:15:19.687Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a0/a9b84a47af06ebed94a1439eb2f02adebfb8628bcd30af1fe3e02f5ef56c/rapidfuzz-3.14.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c84af70bcf34e99aee894e46a0f1ac77f17d0ef828179c387407642e2466d28a", size = 1582448, upload-time = "2026-04-07T11:15:21.98Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f1/5937800238b3f8248e70860d79f69ba8f73e764fff47e36bc9e2f26dbcc6/rapidfuzz-3.14.5-cp313-cp313t-win_arm64.whl", hash = "sha256:aac0ad28c686a5e72b81668b906c030ee28050b244544b8af68e12fb32543895", size = 832932, upload-time = "2026-04-07T11:15:24.358Z" }, + { url = "https://files.pythonhosted.org/packages/81/41/aa3ffb3355e62e1bf91f6599b3092e866bc88487a07c524004943c7676df/rapidfuzz-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1a31cc6d7d03e7318a0974c038959c59e19c752b81115f2e9138b3331cd64d45", size = 1943327, upload-time = "2026-04-07T11:15:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/c2141f1840a41e07ad2db6f724945f8f8ff3065463899a22939152dd6e09/rapidfuzz-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0298d357e2bc59d572da4db0bc631009b6f8f6c9bc8c11e99a12b833f16b6575", size = 1161755, upload-time = "2026-04-07T11:15:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/ca/07/66e753eeaa353161d1d331b7dd517bb349b0bacfebe8496d7b26be26f81f/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59b3dba758661a318995655435c6ab20a04ade79fa51e75bc8dc107cac8df280", size = 1376571, upload-time = "2026-04-07T11:15:31.225Z" }, + { url = "https://files.pythonhosted.org/packages/c8/85/9535df0b78ba51f478c9ce7eb6d1f85535cc31fe356773b48fd9d3e563ca/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4900143d82071bdda533b00300c40b14b963ff826b3642cc463b6dd0f036585e", size = 3156468, upload-time = "2026-04-07T11:15:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/81/ee/b667eb93bba6dc4e0de658edd778e1619dc4d6aab68fa5e5c7f075152735/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:feedf219672eef83ea6be6f3bb093bba396a8560fc75be85ba225f082903df0a", size = 1458311, upload-time = "2026-04-07T11:15:35.557Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ce/479074f5624364a48df3403c538797ef22d3ac49c19dc76c3f79fcdcc70c/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419e4397a36e2665ec992d8d64c20ba4b2a42500c76ecadeca78a4f19cb9cc32", size = 2398228, upload-time = "2026-04-07T11:15:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/a8982f649150fffbdcd6f17565974501f6ab33b2795267bffbd4a7ba905b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:97131ab2be39043054ee28d99e09efe316e6d53449b7e962dfcf3c2de8b2b246", size = 2497226, upload-time = "2026-04-07T11:15:39.857Z" }, + { url = "https://files.pythonhosted.org/packages/19/52/5267c03ef6759831b7d4625a0c9c06e87baa2fae084b61ac9c388858317b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:593c00dac4e30231c35bf3b4f1da8ec0998762e9e94425586a5d636fcd57f9d0", size = 4262283, upload-time = "2026-04-07T11:15:42.279Z" }, + { url = "https://files.pythonhosted.org/packages/71/c0/2579f343a97f5254c43bb5853baccc01488357dcb64a27bcb869b7888a4a/rapidfuzz-3.14.5-cp314-cp314-win32.whl", hash = "sha256:0084b687b02b4e569b46d8d6d4ad25659528e6081cd6d067ca453a69035f07e4", size = 1744614, upload-time = "2026-04-07T11:15:44.498Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/8edfed1e80119dc9c35b11df4bc701eea85622ad681fff0263b6961d3224/rapidfuzz-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:5dfa89d78f22cd773054caff44827b846161a29f2dcf7e78b8f90d086621e502", size = 1588971, upload-time = "2026-04-07T11:15:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/f6/04/5676df93c85cfa57a3045d8047318df9f3cd58c7b8a99340dd95f874795e/rapidfuzz-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:67f3f9d2b444268ab53e47d31bab89954888d23c04c6789f2c727e51fe4b1d13", size = 834985, upload-time = "2026-04-07T11:15:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4a8988cea658fe335048ddef8c876addff1b6daa3c9ca8ad65a5a2196e69/rapidfuzz-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:77eac0526899b3c3ad1454bb2b03cdb491d67358ec8ef0c9c48bd61b632b431d", size = 1972517, upload-time = "2026-04-07T11:15:51.819Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a3/f5cfd9965a9d9a9e32249159797c47b5d6299ea6d1629f9126b25f1c10a3/rapidfuzz-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b9c6bd754d11f6e78ac54e3d86b4b11dc1ba2f13e5fc958899574532897f5a99", size = 1196056, upload-time = "2026-04-07T11:15:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/64/07/561c2e40cfd10e6630a7b0ac5a2a813aef50d944bcd1f3d260319d659d5b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:738c96944d076deeaff70e92b65696ab4f7ecb8081d7791c5403a3257dfaf8ff", size = 1374732, upload-time = "2026-04-07T11:15:56.584Z" }, + { url = "https://files.pythonhosted.org/packages/c2/39/123bb94fee40e2fb3b7c49b80827c7ef42d838e18def3fc2fef5a3cf817a/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4c1bca487a17fe4226b4ffb2d30e799d2b274d692cffa76bd0746f56235fca3", size = 3166902, upload-time = "2026-04-07T11:15:58.768Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/45716fafc9fd2e028cf20b5ac5bc704887081cd312f84edb0e325599414b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:af6a90a4ed2a48fa1a2d17e9d824e6c7c950bea5bad0b707c77fd55751e6bfef", size = 1452130, upload-time = "2026-04-07T11:16:01.453Z" }, + { url = "https://files.pythonhosted.org/packages/ca/49/4e96c413114398481c0a5b0086af32c364a18613c9a2ea578d17c4bea4ee/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bf5018938208d4597b2e679a4f8cff9fd252f1df53583130ae56281a21801b64", size = 2396308, upload-time = "2026-04-07T11:16:03.588Z" }, + { url = "https://files.pythonhosted.org/packages/89/b7/49fea9fc6878d59bd259d01dd1972d9b86117992b1c66d9b16f0a65273c3/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c0919d1f89ddf91129906705723118ea09754171e4116f5a5dbc667c7bc9b261", size = 2488210, upload-time = "2026-04-07T11:16:05.871Z" }, + { url = "https://files.pythonhosted.org/packages/0c/44/a1f732b93ffacbdad077b7c801149549b2938e1bece6addb5ad85ed74df8/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:93d8da883a35116d6813432177f35e570db5b0a5e30ecb0cbd7cb39c815735df", size = 4270621, upload-time = "2026-04-07T11:16:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/ff942d19fce5385054650bb71a58495ddda299d94661ccc4e6e7fa44868b/rapidfuzz-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:0f23e37019ec07712d58976b1ab2b889f8649a7f7c2f626a2f34ea9139e79279", size = 1803950, upload-time = "2026-04-07T11:16:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0f/9aafc63f9661222b819b391c187eed29fc90ad5935f9690e5ecc2d2047a4/rapidfuzz-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7d5ca9c7832e6879a707296d1463685f7c243a27846227044504741640caec66", size = 1632357, upload-time = "2026-04-07T11:16:13.1Z" }, + { url = "https://files.pythonhosted.org/packages/70/a6/51fc1b0e61e3326e1c68a61cfd0c6b3c34c843681c4b1eefbf0596f59162/rapidfuzz-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3e91dcd2549b8f8d843f98ba03a17e01f3d8b72ce942adbbb6761bc58ffce813", size = 855409, upload-time = "2026-04-07T11:16:15.787Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ee/e71853bf82846c5c2174b924b71d8e8099fb05ff87c958a720380b434ba3/rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:578e6051f6d5e6200c259b47a103cf06bb875ab5814d17333fc0b5c290b22f4c", size = 1888603, upload-time = "2026-04-07T11:16:18.223Z" }, + { url = "https://files.pythonhosted.org/packages/36/82/40f67b730f32be2ebad9f62add1571c754f52249254b2e88af094b907eee/rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbf1b8bb2695415b347f3727da1addca2acb82c9b97ac86bebf8b1bead1eb12d", size = 1120599, upload-time = "2026-04-07T11:16:20.682Z" }, + { url = "https://files.pythonhosted.org/packages/ef/9f/a3635cc4ec8fc6e14b46e7db1f7f8763d8c4bef33dcc124eea2e6cb2c8f3/rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4a8f5cc84c7ad6bffa0e9947b33eb343ad66e6b53e94fe54378a5508c5ed53", size = 1348524, upload-time = "2026-04-07T11:16:23.451Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1b/2b229520f0b48464cfcd7aa758f74551d12c9bc4ab544022a60210aab064/rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c6d85283629646fa87acc22c66b30ea9d4de7f6fdf887daa2e30fa041829b5", size = 3099302, upload-time = "2026-04-07T11:16:25.858Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b5/363906b1064fc6fe611783a61764927bbd91919aaaabe8cba82151ca93ef/rapidfuzz-3.14.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:dfef96543ced67d9513a422755db422ae1dc34dade0a1485e0b43e7342ed3ebf", size = 1509889, upload-time = "2026-04-07T11:16:28.487Z" }, +] + +[[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.7.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/13/bbf7d9d1887fe4a3693527c6caa232c197ea9da91f1212e9672eff60329d/regex-2026.7.19-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b", size = 494009, upload-time = "2026-07-19T00:16:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/a3/19/783688e75a2bec15d50aec0d5e7e317d363808bc82a6eb6750b897bfcd7b/regex-2026.7.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52", size = 295287, upload-time = "2026-07-19T00:16:15.702Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/cefe4f051302ca298d3f3e79ed6dbd933ac84485b9515acdd6a52d70cef7/regex-2026.7.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665", size = 290633, upload-time = "2026-07-19T00:16:17.182Z" }, + { url = "https://files.pythonhosted.org/packages/ae/1e/1045ca2cabb12e8ec41ad0d138e9f3ff1eb079d30a6f51ccf7d709b44aad/regex-2026.7.19-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0", size = 785300, upload-time = "2026-07-19T00:16:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/20e5bca184e90bf1bd187efdb53363f4a7b7b34f01d54ced5740caf104bd/regex-2026.7.19-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951", size = 854079, upload-time = "2026-07-19T00:16:19.909Z" }, + { url = "https://files.pythonhosted.org/packages/09/9b/5a2e59678be3b24aa6a42b2c6d66a48daa212593e9f4096fe7ba577fa9b1/regex-2026.7.19-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3", size = 899496, upload-time = "2026-07-19T00:16:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/48/9a/7317f14ed8ed9fd998d1978b4802b07bc4d79216353c435dbcc1ddd1301f/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c", size = 793541, upload-time = "2026-07-19T00:16:22.991Z" }, + { url = "https://files.pythonhosted.org/packages/6f/53/833c2db3e274d3c191f4c42fe5bfa358e4c8b617d5d7312d31334965fc46/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6", size = 785515, upload-time = "2026-07-19T00:16:24.654Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b9/efb2f9fa151d71db09d4015e1fb92fee47416f01c12164836bbd23e2f3c2/regex-2026.7.19-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175", size = 769556, upload-time = "2026-07-19T00:16:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/4d/45610c263f8eadb84e4a1fabd904d81d5176226faa9104ef498bf8a8b285/regex-2026.7.19-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6", size = 774130, upload-time = "2026-07-19T00:16:27.786Z" }, + { url = "https://files.pythonhosted.org/packages/40/95/1b40d87c7a9e5480bec7a87bce9fd67fc3f14b5f106c8ee66d660249072f/regex-2026.7.19-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095", size = 848694, upload-time = "2026-07-19T00:16:29.412Z" }, + { url = "https://files.pythonhosted.org/packages/17/8b/bb45968addd5b394ef9cd9184bd9c65ade1a819dbb2b92b71ad52a0c7907/regex-2026.7.19-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0", size = 758505, upload-time = "2026-07-19T00:16:31.006Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/33386c672fbf43e21602135a0f29a97ee251a483f007fe51d10e9b2dbc93/regex-2026.7.19-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a", size = 836985, upload-time = "2026-07-19T00:16:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/22/f1/9112b86e9bb075619862e8e42b604794389f1958faa68fb69bde505dd90e/regex-2026.7.19-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902", size = 782610, upload-time = "2026-07-19T00:16:33.857Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f9/13d460d8a385ca0b0be9e6be80a90968b9293b3e30895543ad2d1d1653e4/regex-2026.7.19-cp310-cp310-win32.whl", hash = "sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e", size = 266772, upload-time = "2026-07-19T00:16:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/9f/90/29addd7a03e1aea402c1f31467e25c80caabc8c3735b88a23cf73b0aa9c2/regex-2026.7.19-cp310-cp310-win_amd64.whl", hash = "sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db", size = 277967, upload-time = "2026-07-19T00:16:36.847Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ba/ecfce06fe66c122bc6f77ae284887a9282e4411fe1e6268c5266611ca054/regex-2026.7.19-cp310-cp310-win_arm64.whl", hash = "sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6", size = 276963, upload-time = "2026-07-19T00:16:38.315Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cef4de2bac939280b68d32adc659478845238a8274f2f79c465063f590ad/regex-2026.7.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c", size = 494012, upload-time = "2026-07-19T00:16:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/ff/87/e86f51eb117457bb7803132ffe5cb6e2841e2b5bea4cc85d397f3c6e257d/regex-2026.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae", size = 295281, upload-time = "2026-07-19T00:16:41.433Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/2360c41d8080a3d9ec7e5c90fad6eab3b50192869d10e9a5609e48c8177b/regex-2026.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5", size = 290615, upload-time = "2026-07-19T00:16:43.058Z" }, + { url = "https://files.pythonhosted.org/packages/cf/69/b65ba4344efbc771b28fe5dde84cbbb6c8f9551165952fe78def5b9dde6a/regex-2026.7.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20", size = 791804, upload-time = "2026-07-19T00:16:44.662Z" }, + { url = "https://files.pythonhosted.org/packages/81/b6/a40dfa0dc6224b36f620c00296eacc830489cbf8c2837b6750dfe6170375/regex-2026.7.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a", size = 861723, upload-time = "2026-07-19T00:16:46.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/02/735991dee71abd83196a7962f7ed8bf5aa05720ff06e2d3ff896a85e2bbb/regex-2026.7.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7", size = 905932, upload-time = "2026-07-19T00:16:47.956Z" }, + { url = "https://files.pythonhosted.org/packages/45/6c/e7098d8b846ccdbf431d8c081b61e496526a27a28094ed09e0dce21b3f54/regex-2026.7.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc", size = 801407, upload-time = "2026-07-19T00:16:49.43Z" }, + { url = "https://files.pythonhosted.org/packages/8a/18/34b69274e2649bcc7d9b089c2b2983fb2632d8ecf667e359593be9072e79/regex-2026.7.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864", size = 774448, upload-time = "2026-07-19T00:16:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e6/0a72247d025585fd3800b98e040b84d562a88af6303347100484849f4f01/regex-2026.7.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af", size = 783297, upload-time = "2026-07-19T00:16:53.071Z" }, + { url = "https://files.pythonhosted.org/packages/b1/aa/c4f65ae7dd02a36b323a70c4cff326e1f3442361aaebc9311100a130d54f/regex-2026.7.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5", size = 854736, upload-time = "2026-07-19T00:16:54.607Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/668082bcc817b9e694189b84997aeba7385b7779faa6711788679c482e35/regex-2026.7.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82", size = 763298, upload-time = "2026-07-19T00:16:56.289Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fb/2d07ad555e7af88aa5f867fdafa47a8d945ee237c20af3ebceb46a820835/regex-2026.7.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78", size = 844430, upload-time = "2026-07-19T00:16:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/51/15/c82a471fe3dce56f03745635b43aa456c40dc0db089e07ef148b331507d1/regex-2026.7.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327", size = 789683, upload-time = "2026-07-19T00:16:59.583Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f4/7532a2c59d56f5398902c20de60f0c9a5d1cd364e42a051b48e1b210be7b/regex-2026.7.19-cp311-cp311-win32.whl", hash = "sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d", size = 266778, upload-time = "2026-07-19T00:17:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/cf1bc631db154eb95520d9d5dbc2371ff77a0f014bbf7d748fed8496aa63/regex-2026.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965", size = 277983, upload-time = "2026-07-19T00:17:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bd/56ceaf170e875d5a6761bf2bfd0d040f1cacc896850d5e40cb29b11bbd06/regex-2026.7.19-cp311-cp311-win_arm64.whl", hash = "sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e", size = 276961, upload-time = "2026-07-19T00:17:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" }, + { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" }, + { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" }, + { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" }, + { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" }, + { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" }, + { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" }, + { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" }, + { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" }, + { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" }, + { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" }, + { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" }, + { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" }, + { url = "https://files.pythonhosted.org/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd", size = 496750, upload-time = "2026-07-19T00:18:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac", size = 297093, upload-time = "2026-07-19T00:18:41.583Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5", size = 292043, upload-time = "2026-07-19T00:18:43.347Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3", size = 797214, upload-time = "2026-07-19T00:18:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053", size = 866433, upload-time = "2026-07-19T00:18:47.315Z" }, + { url = "https://files.pythonhosted.org/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b", size = 911360, upload-time = "2026-07-19T00:18:49.588Z" }, + { url = "https://files.pythonhosted.org/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a", size = 801275, upload-time = "2026-07-19T00:18:51.767Z" }, + { url = "https://files.pythonhosted.org/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1", size = 777131, upload-time = "2026-07-19T00:18:54.053Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e", size = 785020, upload-time = "2026-07-19T00:18:56.579Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12", size = 861263, upload-time = "2026-07-19T00:18:58.64Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2", size = 766199, upload-time = "2026-07-19T00:19:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97", size = 851317, upload-time = "2026-07-19T00:19:03.015Z" }, + { url = "https://files.pythonhosted.org/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4", size = 789557, upload-time = "2026-07-19T00:19:05.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa", size = 272531, upload-time = "2026-07-19T00:19:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac", size = 280831, upload-time = "2026-07-19T00:19:09.46Z" }, + { url = "https://files.pythonhosted.org/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459", size = 281099, upload-time = "2026-07-19T00:19:11.398Z" }, + { url = "https://files.pythonhosted.org/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3", size = 501121, upload-time = "2026-07-19T00:19:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518", size = 299415, upload-time = "2026-07-19T00:19:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9", size = 294483, upload-time = "2026-07-19T00:19:17.879Z" }, + { url = "https://files.pythonhosted.org/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435", size = 811833, upload-time = "2026-07-19T00:19:20.102Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0", size = 871270, upload-time = "2026-07-19T00:19:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a", size = 917534, upload-time = "2026-07-19T00:19:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276", size = 816135, upload-time = "2026-07-19T00:19:26.919Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c", size = 785492, upload-time = "2026-07-19T00:19:29.192Z" }, + { url = "https://files.pythonhosted.org/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a", size = 800658, upload-time = "2026-07-19T00:19:31.392Z" }, + { url = "https://files.pythonhosted.org/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009", size = 865073, upload-time = "2026-07-19T00:19:33.485Z" }, + { url = "https://files.pythonhosted.org/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218", size = 773684, upload-time = "2026-07-19T00:19:35.599Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966", size = 857769, upload-time = "2026-07-19T00:19:37.738Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44", size = 804546, upload-time = "2026-07-19T00:19:40.229Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78", size = 274526, upload-time = "2026-07-19T00:19:42.398Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2", size = 283763, upload-time = "2026-07-19T00:19:44.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547", size = 283451, upload-time = "2026-07-19T00:19:46.639Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +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/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 = "requests-futures" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/f8/175b823241536ba09da033850d66194c372c65c38804847ac9cef0239542/requests_futures-1.0.2.tar.gz", hash = "sha256:6b7eb57940336e800faebc3dab506360edec9478f7b22dc570858ad3aa7458da", size = 10356, upload-time = "2024-11-15T22:14:51.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/23/7c1096731c15c83826cb0dd42078b561a838aed44c36f370aeb815168106/requests_futures-1.0.2-py2.py3-none-any.whl", hash = "sha256:a3534af7c2bf670cd7aa730716e9e7d4386497554f87792be7514063b8912897", size = 7671, upload-time = "2024-11-15T22:14:50.255Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rouge-score" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { 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 = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad27fea38f3111eb8f02fe75d067f9a985cc358653102/rouge_score-0.1.2.tar.gz", hash = "sha256:c7d4da2683e68c9abf0135ef915d63a46643666f848e558a1b9f7ead17ff0f04", size = 17400, upload-time = "2022-07-22T22:46:22.909Z" } + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +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.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' 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 = "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 = "sounddevice" +version = "0.5.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/db/0c890e2d9aab9ba284021efc02e1d3aebfecab1b611762d7434602209bcf/sounddevice-0.5.6.tar.gz", hash = "sha256:8ec9fbfde2e32f020b167e348f3ab3bac6625a5f15af524d790108ac7147a410", size = 1120094, upload-time = "2026-08-17T07:55:05.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/1f/62eef605172bddc1017508469a12f75bc7c4194ece35c734f822795f53b1/sounddevice-0.5.6-py3-none-any.whl", hash = "sha256:de099612311ad81e55d31ccbd83f43ea6bf4d87b48f9b6ea55a1fbcde0eee4e0", size = 32793, upload-time = "2026-08-17T07:54:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/b6/84/85e719d49cf98b2f406d9ac9c338892286c4448eb42ef0b2625ccf159616/sounddevice-0.5.6-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:e3aef00ad8b1d1740eb66d9a7671eab88a4d2b8fa4ab33498d742e63b65c309c", size = 1009647, upload-time = "2026-08-17T07:54:58.814Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6f/6292145099f72a153a710245f46ae43e5fb6c77bec1b6086cb76c12dc280/sounddevice-0.5.6-py3-none-win32.whl", hash = "sha256:b36b807eb02abd257198bf84b2af05e4fea199a9d2f0019014169c7136d45e9c", size = 1009627, upload-time = "2026-08-17T07:55:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3e/cbc593c31a5f0d817b3fe97e64aa8461bd0f55cb07b67ce1b776296ae336/sounddevice-0.5.6-py3-none-win_amd64.whl", hash = "sha256:7f4162f514f007b0bf25a3ccfed3f1705bc2ec311888a90232729eec4f57a4f4", size = 1009630, upload-time = "2026-08-17T07:55:02.088Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/b0c21c9f215a6fd9606b8f8748c21212dc098e5d5a2d93068c50edcf19b4/sounddevice-0.5.6-py3-none-win_arm64.whl", hash = "sha256:c8ae19173e5f27f8c12d4b5eee2dbfe542cee125d591e663e0fb4dfb75246d45", size = 1009630, upload-time = "2026-08-17T07:55:03.689Z" }, +] + +[[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/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/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]] +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]] +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]] +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" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "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 = "tokenizers" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +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/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 = "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" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +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/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 = "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]] +name = "types-protobuf" +version = "7.34.1.20260816" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/86/f592845ff8410583687e6dbf985a028e6ec6d378d0bdf548817aa39937b9/types_protobuf-7.34.1.20260816.tar.gz", hash = "sha256:6f43846e7a3cc2621abfa79e2bb03461e6769a74cdae7462efaddf84580fd5c1", size = 69170, upload-time = "2026-08-16T02:50:00.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/cc/909e791f11de77c2073007dbcce8b502865ef12c7881bdf36b0ccb206ab4/types_protobuf-7.34.1.20260816-py3-none-any.whl", hash = "sha256:2e3a225b3c21f0022daa34cec155f03858bb9aff9792947b8e71f0e9f1791fdb", size = 86036, upload-time = "2026-08-16T02:49:59.807Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.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 = "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 = "uvicorn" +version = "0.52.3" +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/2e/28/64ca011edf31c715b4fad359c587ea52391aaffa125065695590241ff617/uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58", size = 100621, upload-time = "2026-08-13T16:50:02.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" }, +] + +[[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 = "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 = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[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" }, +] diff --git a/oss/simulation-acceptance/voice_cases.py b/oss/simulation-acceptance/voice_cases.py index 9fa2c4d..a517d1f 100644 --- a/oss/simulation-acceptance/voice_cases.py +++ b/oss/simulation-acceptance/voice_cases.py @@ -197,6 +197,29 @@ def missing_env(case: VoiceCase) -> list[str]: return [name for name in case.required_env if not os.environ.get(name, "").strip()] +def _harness_scenario() -> "simulate.Scenario | None": + """The caller the harness prepared, if this run is driving one of its scenarios. + + ``HARNESS_INSTRUCTION`` is the simulator prompt the environment step wrote with this + scenario's values already filled in, so nothing about how a caller behaves is decided here. + Without it the built-in acceptance persona is used and this file behaves exactly as before. + """ + instruction = os.environ.get("HARNESS_INSTRUCTION", "").strip() + if not instruction: + return None + return simulate.Scenario( + name=os.environ.get("HARNESS_SCENARIO", "harness"), + dataset=[ + simulate.Persona( + persona={"name": "customer"}, + situation=instruction, + outcome=os.environ.get("HARNESS_OUTCOME", "") + or "Do what you came to do, or accept that you cannot.", + ) + ], + ) + + def build_inputs(case_id: str, run_id: str) -> VoiceInputs: case = CASES[case_id] room_override = os.environ.get("ACCEPTANCE_ROOM_NAME_OVERRIDE", "").strip() @@ -206,7 +229,7 @@ def build_inputs(case_id: str, run_id: str) -> VoiceInputs: room_mode="managed", room_name_verbatim=bool(room_override), ) - scenario = simulate.Scenario( + scenario = _harness_scenario() or simulate.Scenario( name=f"acceptance-{case_id}", dataset=[ simulate.Persona( diff --git a/pyproject.toml b/pyproject.toml index dc32475..0e938ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dependencies = [ + "claude-agent-sdk>=0.2.139", "fi-instrumentation-otel>=0.1.16", "gepa>=0.0.17", "httpx>=0.24.0", @@ -87,6 +88,13 @@ a2a = [ ] nli = ["transformers>=5.2.0,<6", "torch>=2.10.0,<3"] embeddings = ["sentence-transformers>=5.2.3,<6"] +# What `harness-ui/server.py` needs to serve the harness over HTTP. Declared rather than left to +# whatever happens to be in the environment: uv sync removes anything undeclared, so an ad hoc +# install of these disappears the first time somebody syncs. +harness-ui = [ + "fastapi>=0.115,<1", + "uvicorn>=0.30,<1", +] feedback = ["chromadb>=0.4.0"] notebook = [ "ipykernel>=6", # kernel for examples/agent_learning_sdk_demo.ipynb @@ -149,6 +157,10 @@ dev = [ "hatchling>=1.25", "pytest>=8.3", "ruff>=0.9", + # Also in the harness-ui extra. Repeated here so that syncing without that extra does not + # uninstall the server out from under a working checkout. + "fastapi>=0.115,<1", + "uvicorn>=0.30,<1", ] [tool.pytest.ini_options] diff --git a/scripts/replay_ground_truth.py b/scripts/replay_ground_truth.py new file mode 100644 index 0000000..4cb6ae7 --- /dev/null +++ b/scripts/replay_ground_truth.py @@ -0,0 +1,173 @@ +"""Replay an external benchmark's hand-written trajectories against a world we generated. + +Every gate in this harness so far is one we wrote, checking work we produced. That is worth +something, but it cannot answer the question that actually matters about a generated +environment: **is it faithful to the agent it was built from?** + +An independent benchmark answers it. Sierra's tau-bench ships hand-written tasks, each with the +exact tool calls a correct agent should make. Those trajectories were written by people who had +never seen this harness, against the real implementation. Replaying them through a world the +harness built automatically from the same source is therefore an external check: if the world is +faithful, the trajectories run clean; where they do not, the difference is a real defect in the +world and it is pointed at directly. + + .venv/bin/python scripts/replay_ground_truth.py \ + --world artifacts/environments/tau_retail \ + --tasks .../tau-bench/tau_bench/envs/retail/tasks_test.py + +What it reports, per trajectory: every call accepted, or the first one the world refused or +crashed on. A refusal is the interesting case — either the trajectory relies on data our sample +does not have, or a handler is stricter than the real tool. +""" + +from __future__ import annotations + +import argparse +import ast +import json +import sys +from dataclasses import dataclass, field +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) + +from fi.alk.harness.world.snapshot import restore # noqa: E402 + + +@dataclass +class Trajectory: + """One hand-written task: what the user wanted, and the calls a correct agent makes.""" + + instruction: str + actions: list[tuple[str, dict]] = field(default_factory=list) + + +def read_tasks(path: Path) -> list[Trajectory]: + """The trajectories, read from the benchmark's own Python without importing it. + + Parsed rather than imported: importing would pull in the benchmark's package and its + dependencies, and all that is wanted here is literal data it already states plainly. + """ + tree = ast.parse(path.read_text(encoding="utf-8")) + found: list[Trajectory] = [] + for node in ast.walk(tree): + if not (isinstance(node, ast.Call) and getattr(node.func, "id", "") == "Task"): + continue + instruction, actions = "", [] + for keyword in node.keywords: + if keyword.arg == "instruction" and isinstance(keyword.value, ast.Constant): + instruction = str(keyword.value.value) + if keyword.arg == "actions" and isinstance(keyword.value, ast.List): + for entry in keyword.value.elts: + if not ( + isinstance(entry, ast.Call) + and getattr(entry.func, "id", "") == "Action" + ): + continue + name, kwargs = "", {} + for field_ in entry.keywords: + if field_.arg == "name" and isinstance(field_.value, ast.Constant): + name = str(field_.value.value) + if field_.arg == "kwargs": + try: + kwargs = ast.literal_eval(field_.value) + except ValueError: + kwargs = {} + if name: + actions.append((name, kwargs)) + if actions: + found.append(Trajectory(instruction=instruction, actions=actions)) + return found + + +@dataclass +class Replay: + index: int + steps: int = 0 + accepted: int = 0 + stopped_at: str = "" + why: str = "" + crashed: bool = False + + @property + def clean(self) -> bool: + return not self.stopped_at + + +def replay(trajectory: Trajectory, world_root: Path, index: int) -> Replay: + """One trajectory against its own fresh copy of the world.""" + result = Replay(index=index, steps=len(trajectory.actions)) + world = restore(world_root) + try: + world.reset() + for name, arguments in trajectory.actions: + call = world.call(name, arguments) + if call.ok: + result.accepted += 1 + continue + result.stopped_at = f"{name}({json.dumps(arguments, default=str)[:120]})" + result.why = call.error + result.crashed = not call.refused + break + finally: + world.close() + return result + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--world", required=True, help="a built environment") + parser.add_argument("--tasks", required=True, help="the benchmark's tasks file") + parser.add_argument("--limit", type=int, default=0, help="only the first N trajectories") + parser.add_argument("--show", type=int, default=12, help="how many failures to detail") + args = parser.parse_args(argv) + + world_root = Path(args.world) + if not (world_root / "world.sqlite").exists(): + print(f"no world at {world_root}. Run `build` first.", file=sys.stderr) + return 1 + + trajectories = read_tasks(Path(args.tasks)) + if args.limit: + trajectories = trajectories[: args.limit] + if not trajectories: + print("no trajectories found in that file", file=sys.stderr) + return 1 + + results = [replay(one, world_root, index) for index, one in enumerate(trajectories)] + clean = [one for one in results if one.clean] + crashed = [one for one in results if one.crashed] + calls = sum(one.steps for one in results) + accepted = sum(one.accepted for one in results) + + print(f"world: {world_root}") + print(f"trajectories: {len(results)} hand-written, from {Path(args.tasks).name}") + print(f"replayed: {len(clean)}/{len(results)} clean") + print(f"calls: {accepted}/{calls} accepted by the world") + if crashed: + print(f"crashes: {len(crashed)} — these are defects in the world, not refusals") + + failed = [one for one in results if not one.clean] + if failed: + print("\nwhere they stopped:") + for one in failed[: args.show]: + mark = "CRASH" if one.crashed else "refused" + print(f" [{one.index}] {mark} after {one.accepted}/{one.steps}: {one.stopped_at}") + print(f" {one.why[:160]}") + if len(failed) > args.show: + print(f" … and {len(failed) - args.show} more") + + # The tools a real suite actually exercises, which is what our own coverage is measured + # against: a generated suite that never reaches the write tools has not tested the agent. + used: dict[str, int] = {} + for one in trajectories: + for name, _ in one.actions: + used[name] = used.get(name, 0) + 1 + print("\nwhat the hand-written trajectories exercise:") + for name, count in sorted(used.items(), key=lambda pair: -pair[1]): + print(f" {count:4} {name}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/uv.lock b/uv.lock index c07914c..edbc4fc 100644 --- a/uv.lock +++ b/uv.lock @@ -54,6 +54,7 @@ name = "agent-learning-kit" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "claude-agent-sdk" }, { name = "fi-instrumentation-otel" }, { name = "gepa" }, { name = "httpx" }, @@ -100,6 +101,10 @@ embeddings = [ feedback = [ { name = "chromadb" }, ] +harness-ui = [ + { name = "fastapi" }, + { name = "uvicorn" }, +] langchain = [ { name = "langchain-core" }, { name = "langgraph" }, @@ -136,9 +141,11 @@ trinity = [ [package.dev-dependencies] dev = [ { name = "build" }, + { name = "fastapi" }, { name = "hatchling" }, { name = "pytest" }, { name = "ruff" }, + { name = "uvicorn" }, ] [package.metadata] @@ -152,6 +159,8 @@ requires-dist = [ { name = "audioop-lts", marker = "python_full_version >= '3.13' and extra == 'trinity'", specifier = ">=0.2.1" }, { name = "chromadb", marker = "extra == 'all'", specifier = ">=0.4.0" }, { name = "chromadb", marker = "extra == 'feedback'", specifier = ">=0.4.0" }, + { name = "claude-agent-sdk", specifier = ">=0.2.139" }, + { name = "fastapi", marker = "extra == 'harness-ui'", specifier = ">=0.115,<1" }, { name = "fi-instrumentation-otel", specifier = ">=0.1.16" }, { name = "gepa", specifier = ">=0.0.17" }, { name = "httpx", specifier = ">=0.24.0" }, @@ -193,15 +202,18 @@ requires-dist = [ { name = "transformers", marker = "extra == 'all'", specifier = ">=5.2.0,<6" }, { name = "transformers", marker = "extra == 'nli'", specifier = ">=5.2.0,<6" }, { name = "typer", specifier = ">=0.9.0,<1.0.0" }, + { name = "uvicorn", marker = "extra == 'harness-ui'", specifier = ">=0.30,<1" }, ] -provides-extras = ["simulate", "evaluation", "optimize", "livekit", "langchain", "pipecat", "mcp", "a2a", "nli", "embeddings", "feedback", "notebook", "trinity", "all"] +provides-extras = ["simulate", "evaluation", "optimize", "livekit", "langchain", "pipecat", "mcp", "a2a", "nli", "embeddings", "harness-ui", "feedback", "notebook", "trinity", "all"] [package.metadata.requires-dev] dev = [ { name = "build", specifier = ">=1.5" }, + { name = "fastapi", specifier = ">=0.115,<1" }, { name = "hatchling", specifier = ">=1.25" }, { name = "pytest", specifier = ">=8.3" }, { name = "ruff", specifier = ">=0.9" }, + { name = "uvicorn", specifier = ">=0.30,<1" }, ] [[package]] @@ -897,6 +909,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/ce/0f7be6e5d0feafa2cda54b12e6542afeea7dea89d2d411e14da90f8abb96/chromadb-1.5.9-cp39-abi3-win_amd64.whl", hash = "sha256:4fd0b560e56761b7f3cb4d5c6205fd5f20814484b4a3e4e9af9038c2b428fc6c", size = 23542454, upload-time = "2026-05-05T05:54:54.942Z" }, ] +[[package]] +name = "claude-agent-sdk" +version = "0.2.139" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "mcp" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/b6/cfcdefed1f866a8ba372ef3884c8020dd54338d15d8b45d5a1ff7432cea1/claude_agent_sdk-0.2.139.tar.gz", hash = "sha256:4395ed541cdd4c13aeb1213b3b414b7e8a94cc060a773137e961882e81c174a7", size = 319519, upload-time = "2026-08-14T22:34:48.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/7f/f04c33553cbc69bb96d045dc38a6266726fad72130f22f405dfe9eb54bf1/claude_agent_sdk-0.2.139-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbc50cc475ec633cabfa36347646097e9b1466d53130e4a04a87308ff830c87b", size = 88043656, upload-time = "2026-08-14T22:34:53.027Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/a17f5318ca0220479f20fdf83fa54a838a0a13ee203495ff67c72c3f43a7/claude_agent_sdk-0.2.139-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:1c08206b1603444582cd365effaf95d2a8248661f1492281fb2d529b0887c047", size = 93000433, upload-time = "2026-08-14T22:34:58.225Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2e/5bcec31700d76ad2d5b9fc28521a75a66f464063dac11373cf8d61446a4f/claude_agent_sdk-0.2.139-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:e69ae1a0b2af684c64839cc16e10b70800d9d2f57622b8c0d1739dd878cd7346", size = 97396659, upload-time = "2026-08-14T22:35:03.734Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7f/582b3c1936c9f4ebc1bdc55a3923f1b680ef3c01928ffff1ea38eb84f637/claude_agent_sdk-0.2.139-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:34b289b3436fe24013f7b9cfe9f0a4e0806917a9ef8bbe829cda9a7b12d41a77", size = 98391889, upload-time = "2026-08-14T22:35:09.683Z" }, + { url = "https://files.pythonhosted.org/packages/56/54/d94af31d19b4e8d63d1b15002fd333ea77a040b7ab7a388044e511c8f9f6/claude_agent_sdk-0.2.139-py3-none-win_amd64.whl", hash = "sha256:9b76f0ffe216d6ca290d5f4f295ecb030dc496f101986ac99480a89d4abc6426", size = 100746507, upload-time = "2026-08-14T22:35:15.144Z" }, +] + [[package]] name = "click" version = "8.4.1" @@ -1189,6 +1220,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + [[package]] name = "fastjsonschema" version = "2.22.1"