diff --git a/.github/workflows/evals.yml b/.github/workflows/evals.yml new file mode 100644 index 00000000..bc69b7e6 --- /dev/null +++ b/.github/workflows/evals.yml @@ -0,0 +1,258 @@ +name: Anton evals + +# Behavioural baseline for the Anton harness: runs the `dialog_context` dataset +# through a cowork-server built from this PR's anton sha, then posts accuracy, +# tokens and latency as a PR comment. +# +# This reports numbers; it does not gate. Agent runs vary between passes, and a +# red check on noise trains people to click "re-run" without reading. The job +# fails only when it could not measure anything at all. +# +# Required repository secrets: +# MINDSHUB_API_KEY prod gateway key — also seeded into the built cowork-server. +# LANGFUSE_HOST / LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY +# the project the PROD gateway writes to. A dev-project key +# here yields empty token/cost columns, not an error. +# GH_EVALS_READ_TOKEN +# read access to mindsdb/cowork-server + mindsdb/cowork_evals. +# The automatic GITHUB_TOKEN is scoped to this repository +# alone — same-org membership does not extend it. +# +# Credentials come from secrets ONLY. This repository is public, and +# workflow_dispatch inputs are recorded on the run for anyone to read, so a key +# typed into the launch form would be published. The inputs below are all +# non-sensitive by design. + +permissions: + contents: read + pull-requests: write # the report is posted as a PR comment + +on: + # `labeled` fires once, when the label is added — later pushes to the PR do not + # re-trigger it. Re-running is "remove the label, add it again", which is the + # explicit opt-in a paid run should have. + pull_request: + types: [labeled] + workflow_dispatch: + inputs: + anton_ref: + description: "anton ref to build (default: this branch)" + required: false + cowork_ref: + description: "cowork-server ref to build" + default: staging + baseline_ref: + description: "anton ref to compare against" + default: staging + evals_ref: + description: "cowork_evals ref providing the run config and the report" + default: main + repeats: + description: "replicate passes per task" + default: "3" + +concurrency: + group: evals-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + evals: + if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'run-evals' + runs-on: ubuntu-latest + # Two builds and two passes over the dataset: ~6 minutes of agent time each, + # plus a `uv sync` per harness build. Anything past this is hung rather than + # slow, and the GitHub default of 360 would hold a runner for six hours to + # establish that. + timeout-minutes: 120 + env: + COWORK_REF: ${{ inputs.cowork_ref || 'staging' }} + BASELINE_REF: ${{ inputs.baseline_ref || 'staging' }} + EVALS_REF: ${{ inputs.evals_ref || 'main' }} + REPEATS: ${{ inputs.repeats || '3' }} + ANTON_SHA: ${{ github.event.pull_request.head.sha || inputs.anton_ref || github.sha }} + REPO_TOKEN: ${{ secrets.GH_EVALS_READ_TOKEN }} + LANGFUSE_HOST: ${{ secrets.LANGFUSE_HOST }} + LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }} + LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }} + steps: + # `pull_request` withholds secrets from forks by design, so a fork PR cannot + # run this and must not be failed for it — but it must not look measured + # either. + - name: Skip fork PRs + id: fork + if: github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name != github.repository + run: | + echo "skip=1" >> "$GITHUB_OUTPUT" + { + echo "### Evals skipped — fork PR" + echo "" + echo "\`pull_request\` withholds repository secrets from forks, so no" + echo "numbers were measured for this change. Re-run the eval from a" + echo "first-party branch before merging." + } >> "$GITHUB_STEP_SUMMARY" + + # Checked before the checkouts so a missing credential reads as "nobody + # provisioned this" rather than a confusing 404 on a repository that does + # exist. + - name: Fail loudly on a missing credential + if: steps.fork.outputs.skip != '1' + env: + MINDS_API_KEY: ${{ secrets.MINDSHUB_API_KEY }} + run: | + set -euo pipefail + : "${MINDS_API_KEY:?MINDSHUB_API_KEY secret is not set}" + : "${REPO_TOKEN:?no read token — set the GH_EVALS_READ_TOKEN secret or pass repo_token}" + # All three or none — a partial Langfuse config silently drops to empty + # token/cost columns downstream instead of the clear error this step + # exists to give. + : "${LANGFUSE_HOST:?no Langfuse host — token and cost columns would be empty}" + : "${LANGFUSE_PUBLIC_KEY:?no Langfuse public key — token and cost columns would be empty}" + : "${LANGFUSE_SECRET_KEY:?no Langfuse secret key — token and cost columns would be empty}" + + - name: Check out anton + if: steps.fork.outputs.skip != '1' + uses: actions/checkout@v4 + with: + path: anton + ref: ${{ env.ANTON_SHA }} + # The eval builds its harness with `git worktree add `, which needs + # the object present locally — a shallow clone does not have it. + fetch-depth: 0 + + # Pinned to a commit, not a branch name, for two reasons: + # - the eval compares the two refs to decide whether the baseline build is + # a duplicate, and "staging" never equals a sha, so the check would never + # fire on a PR that IS staging; + # - a branch that moves mid-run would make the two halves of the comparison + # disagree about what "baseline" meant. + # `pull_request` runs already give ANTON_SHA as a sha; a manual `workflow_dispatch` + # with `anton_ref: staging` does not — resolve it from the checkout above so + # cowork_evals' duplicate-variant check (a raw string compare of the two refs) + # sees the same sha on both sides when anton_ref and BASELINE_REF are the same + # commit, instead of "staging" vs a resolved sha never matching. + - name: Resolve the anton commit + if: steps.fork.outputs.skip != '1' + working-directory: anton + run: | + set -euo pipefail + echo "ANTON_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + + - name: Resolve the baseline commit + id: baseline + if: steps.fork.outputs.skip != '1' + working-directory: anton + run: | + set -euo pipefail + git fetch --no-tags --quiet origin "$BASELINE_REF" + sha=$(git rev-parse --verify FETCH_HEAD^{commit}) + echo "resolved $BASELINE_REF -> $sha" + echo "sha=$sha" >> "$GITHUB_OUTPUT" + + - name: Check out cowork-server + if: steps.fork.outputs.skip != '1' + uses: actions/checkout@v4 + with: + repository: mindsdb/cowork-server + ref: ${{ env.COWORK_REF }} + token: ${{ env.REPO_TOKEN }} + path: cowork-server + fetch-depth: 0 + + - name: Check out cowork_evals + if: steps.fork.outputs.skip != '1' + uses: actions/checkout@v4 + with: + repository: mindsdb/cowork_evals + ref: ${{ env.EVALS_REF }} + token: ${{ env.REPO_TOKEN }} + path: cowork_evals + + - name: Install uv + if: steps.fork.outputs.skip != '1' + uses: astral-sh/setup-uv@v5 + with: + # The managed build runs `uv sync` in a fresh cowork-server worktree on + # every new anton sha, so the build dir itself is never reused. uv's + # package cache is the part that carries over. + enable-cache: true + cache-dependency-glob: "**/uv.lock" + + - name: Run the eval + id: eval + if: steps.fork.outputs.skip != '1' + working-directory: cowork_evals + env: + MINDS_URL: https://api.mindshub.ai/v1 + MINDS_API_KEY: ${{ secrets.MINDSHUB_API_KEY }} + CI_ANTON_REPO: ${{ github.workspace }}/anton + CI_COWORK_REPO: ${{ github.workspace }}/cowork-server + CI_ANTON_REF: ${{ env.ANTON_SHA }} + CI_COWORK_REF: ${{ env.COWORK_REF }} + # The build to compare against. When it is the same commit as + # CI_ANTON_REF the eval drops the duplicate and reports a single row. + CI_BASELINE_REF: ${{ steps.baseline.outputs.sha }} + run: | + set -euo pipefail + uv run eval run \ + --config runs/templates/ci-dialog-context.yaml \ + --repeats "$REPEATS" | tee run.log + # `eval run` ends with "done: (...)"; that id names the results + # directory everything downstream reads. + run_id=$(sed -n 's/^done: \([^ ]*\).*/\1/p' run.log | tail -1) + [ -n "$run_id" ] || { echo "::error::eval run printed no run id"; exit 1; } + echo "run_id=$run_id" >> "$GITHUB_OUTPUT" + + - name: Render the report + id: report + if: steps.fork.outputs.skip != '1' + working-directory: cowork_evals + run: | + set -euo pipefail + # Derived, not hardcoded: adding a task to the dataset must not silently + # lower the bar for what counts as a complete run. + tasks=$(grep -c . datasets/dialog_context.jsonl) + expected=$(( tasks * REPEATS )) + uv run eval report --run "${{ steps.eval.outputs.run_id }}" \ + --fmt pr --expect "$expected" > report.md + cat report.md >> "$GITHUB_STEP_SUMMARY" + + - name: Comment on the PR + if: steps.fork.outputs.skip != '1' && github.event_name == 'pull_request' + working-directory: cowork_evals + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} + SHA: ${{ env.ANTON_SHA }} + run: | + set -euo pipefail + marker='' + { + echo "$marker" + cat report.md + echo "" + echo "anton \`${SHA:0:8}\` · cowork-server \`$COWORK_REF\` · $REPEATS replicates" + } > comment.md + # One comment per PR, edited in place: a fresh comment per run buries the + # conversation under near-identical tables. --paginate covers PRs past the + # 30-comment first page; --jq runs per page, but an empty page contributes + # no output, so the concatenated result is still just the one matching id. + id=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \ + --jq "[.[] | select(.body | startswith(\"$marker\")) | .id] | first // empty") + if [ -n "$id" ]; then + gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -F body=@comment.md + else + gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -F body=@comment.md + fi + + # Kept even when the run fails: a broken run's logs and partial results are + # the only way to tell a harness bug from a gateway outage. + - name: Upload run artifacts + if: always() && steps.fork.outputs.skip != '1' + uses: actions/upload-artifact@v4 + with: + name: eval-run-${{ steps.eval.outputs.run_id || github.run_id }} + path: | + cowork_evals/run.log + cowork_evals/runs/ + retention-days: 90 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62bdaf53..35259581 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ name: Auto-release and publish to PyPI # Stable release stream: on every push to main, validate, cut a CalVer release, -# publish it to PyPI, and run live release e2e. Staging pre-releases (rc) live in +# verify the built wheel, then publish it to PyPI. Staging pre-releases (rc) live in # publish-staging.yml so each stream keeps its own run tree, permissions, # notifications, and publishing identity. # @@ -42,54 +42,49 @@ jobs: calver-major: "2" runs-on: ubuntu-latest + verify-wheel: + needs: auto-release + permissions: + contents: read + uses: ./.github/workflows/tests_e2e_release.yml + with: + tag: ${{ needs.auto-release.outputs.tag }} + version: ${{ needs.auto-release.outputs.version }} + secrets: inherit + publish: name: Publish to PyPI - needs: auto-release + # Gated on verify-wheel: a PyPI upload cannot be withdrawn. + needs: [auto-release, verify-wheel] runs-on: ubuntu-latest environment: pypi permissions: contents: read id-token: write # required for trusted publisher (OIDC) steps: - - uses: actions/checkout@v4 + # Publish the bytes verify-wheel actually tested, not a rebuild of the + # same source — a second build verifies equivalence, not the artifact. + - name: Download the verified distributions + uses: actions/download-artifact@v4 with: - ref: ${{ needs.auto-release.outputs.tag }} - fetch-depth: 0 # hatch-vcs needs tags to derive version + name: verified-dist + path: dist - - name: Setup uv - uses: astral-sh/setup-uv@v5 - with: - python-version: "3.12" - - - name: Build package - env: - # A re-run or re-dispatch on an already-tagged head leaves two CalVer - # tags on one commit and `git describe` resolves the older one; build - # exactly the version the release job minted. - SETUPTOOLS_SCM_PRETEND_VERSION: ${{ needs.auto-release.outputs.version }} - run: uv build + - name: Show what is being published + run: ls -l dist - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 # Trusted publisher (OIDC) — release.yml must be registered at the # anton-agent PyPI project's publishing settings (environment: pypi). - e2e: - needs: auto-release - permissions: - contents: read - uses: ./.github/workflows/tests_e2e_release.yml - with: - tag: ${{ needs.auto-release.outputs.tag }} - secrets: inherit - notify: # Alert the eng channel if ANY job in the release pipeline fails (tests, tag, - # PyPI publish, or release e2e). + # wheel verification, or PyPI publish). # One job covers both outcomes: a `uses:` job cannot branch on status, so # the aggregate result picks the failed or recovered message, and a # cancelled run stays silent. - needs: [unit-tests, auto-release, publish, e2e] + needs: [unit-tests, auto-release, verify-wheel, publish] if: ${{ github.ref == 'refs/heads/main' && !cancelled() && !contains(needs.*.result, 'cancelled') }} permissions: contents: read diff --git a/.github/workflows/scratchpad-dev-build.yml b/.github/workflows/scratchpad-dev-build.yml index 1a7903b7..78103442 100644 --- a/.github/workflows/scratchpad-dev-build.yml +++ b/.github/workflows/scratchpad-dev-build.yml @@ -6,6 +6,11 @@ name: Scratchpad image - Dev build on PR # rebuilds. The scratchpad-controller references the resulting tag via # SCRATCHPAD_CONTROLLER__SCRATCHPAD_IMAGE; there is no Helm chart for this image, so this # workflow only builds and scans (no deploy job). +# +# The image is smoke-tested inside the build, not here: the Dockerfile's last layer runs +# docker/image_smoke.py as UID 1000, which executes both entrypoints the controller execs. +# It sits in the build rather than in a step below it so a failure blocks the PUSH — an +# image no pod can serve a turn with never reaches ECR to be pinned by mistake. on: pull_request: diff --git a/.github/workflows/tests_e2e_release.yml b/.github/workflows/tests_e2e_release.yml index c8192375..faa3f9b8 100644 --- a/.github/workflows/tests_e2e_release.yml +++ b/.github/workflows/tests_e2e_release.yml @@ -1,5 +1,9 @@ name: Release e2e scenarios +# Verifies the artifact, not the checkout: builds the wheel for the tagged +# commit, installs it into a clean venv, and runs the live E2E scenarios +# through that install. Publishing is irreversible, so this must gate it. + permissions: contents: read @@ -7,15 +11,24 @@ on: workflow_call: inputs: tag: - description: "Release tag the caller just created (e.g. v2.0.5)" + description: "Release tag to verify (e.g. v2.0.5)" + required: true + type: string + version: + description: "Version the release job minted (e.g. 2.0.5)" required: true type: string jobs: - e2e-live: + verify-wheel: runs-on: ubuntu-latest + env: + WHEEL_VENV: /tmp/wheelenv steps: - uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag }} + fetch-depth: 0 # hatch-vcs needs tags to derive version - name: Install uv uses: astral-sh/setup-uv@v5 @@ -23,11 +36,85 @@ jobs: - name: Set up Python run: uv python install 3.12 - - name: Run E2E tests (live) + - name: Build the distributions + env: + # Match the publish job: build exactly the version that was minted. + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ inputs.version }} + run: uv build + + - name: Install the wheel into a clean venv + run: | + uv venv --python 3.12 "$WHEEL_VENV" + uv pip install --python "$WHEEL_VENV/bin/python" dist/*.whl + + - name: Wheel must not resolve to the source tree + # A green run that imported anton/ from the repo would prove nothing. + run: | + loc=$(cd /tmp && "$WHEEL_VENV/bin/python" -c \ + "import anton, pathlib; print(pathlib.Path(anton.__file__).resolve())") + echo "anton imported from: $loc" + case "$loc" in + "$WHEEL_VENV"/*) ;; + *) echo "::error::anton resolved outside the wheel venv: $loc"; exit 1 ;; + esac + + - name: Installed version matches the release env: + EXPECTED: ${{ inputs.version }} + # Compare as PEP 440 versions: packaging strips leading zeros, so the + # CalVer tag 2026.08.31.4 installs as 2026.8.31.4. + run: | + cd /tmp && "$WHEEL_VENV/bin/python" - <<'PY' + import os, sys + from packaging.version import Version + import anton + got, expected = anton.__version__, os.environ["EXPECTED"] + print(f"installed={got} expected={expected}") + if Version(got) != Version(expected): + sys.exit(f"::error::wheel reports {got}, release minted {expected}") + PY + + - name: Console script entry point works + # `anton` is what users type; the E2E harness uses `python -m anton`. + run: cd /tmp && "$WHEEL_VENV/bin/anton" --help > /dev/null + + - name: Sdist builds, installs and runs + # uv build produces a wheel and an sdist, and both get published, so + # both must be verified. + run: | + uv venv --python 3.12 /tmp/sdistenv + uv pip install --no-cache --python /tmp/sdistenv/bin/python dist/*.tar.gz + cd /tmp && /tmp/sdistenv/bin/anton --help > /dev/null + + - name: Installer path resolves and runs on a cold cache + # install.sh installs from the git tag, not the wheel, and a warm local + # env can hide a dependency that is no longer pulled in transitively. + run: | + uv venv --python 3.12 /tmp/gitenv + uv pip install --no-cache --python /tmp/gitenv/bin/python \ + "git+https://github.com/${{ github.repository }}.git@${{ inputs.tag }}" + cd /tmp && /tmp/gitenv/bin/anton --help > /dev/null + + - name: Run E2E scenarios against the installed wheel (live) + env: + ANTON_E2E_WHEEL_PYTHON: ${{ env.WHEEL_VENV }}/bin/python ANTON_OPENAI_API_KEY: ${{ secrets.ANTON_OPENAI_API_KEY }} ANTON_PLANNING_PROVIDER: openai ANTON_CODING_PROVIDER: openai ANTON_PLANNING_MODEL: gpt-4.1-mini ANTON_CODING_MODEL: gpt-4.1-mini run: uv run --group dev pytest tests/e2e/ --live -v + + # Hand the publish job these exact bytes. Last step, so a failed + # verification never leaves a publishable artifact behind. + - name: Upload the verified distributions + uses: actions/upload-artifact@v4 + with: + name: verified-dist + path: dist/ + if-no-files-found: error + # Long enough that a delayed or re-run publish (environment approval, + # transient PyPI failure retried the next day) still finds the bytes. + # If it has expired anyway, re-run this whole workflow for the tag — + # never rebuild in the publish job, that skips verification. + retention-days: 14 diff --git a/Dockerfile b/Dockerfile index d7fd77a6..7a0a80b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,5 +69,11 @@ RUN printf '#!/bin/sh\nexec python -m anton.core.backends.scratchpad_boot\n' \ RUN useradd -u 1000 -m -s /bin/sh scratchpad USER 1000 +# Run both entrypoints once, as the pod's uid, before this image can be pushed. +# The build is otherwise green for an image no pod can serve a turn with: every +# check up to here proves the image was ASSEMBLED, none proves it RUNS. See +# docker/image_smoke.py for what each check is guarding against. +RUN python /app/docker/image_smoke.py + # The controller always execs an explicit command; this default keeps the image runnable standalone. CMD ["python", "-m", "anton.cloud_turn"] diff --git a/anton/analytics.py b/anton/analytics.py index a4d7d644..b0ebc7f9 100644 --- a/anton/analytics.py +++ b/anton/analytics.py @@ -171,11 +171,20 @@ # spend-ceiling extensions granted this turn, and the # ceiling one's size), verifier_failure + verifier_error_type # (WHY the completion verifier produced no verdict — the -# loop's truncated/transient/hard/denied class and the +# loop's truncated/transient/hard/denied class, or +# latched_{hard,truncated,denied,mixed} for a turn that made +# no call because an earlier one latched, plus the # content-free exception type; "" on verified turns; ENG-1858), # harness, surface (desktop / web / cli — WHERE # the user was, "" when the host did not say; ENG-1945), -# anton_version, conversation_id, turn_index +# anton_version, conversation_id, turn_index, +# turn_attempt_id (unique per turn EXECUTION — `turn_index` +# is only a position in the history and REPEATS across a +# retried or cancelled attempt, so the pair +# (conversation_id, turn_index) is NOT a unique key; +# ENG-2243). Count attempts with turn_attempt_id, turns with +# (conversation_id, turn_index). Absent on pre-ENG-2243 +# builds — read as unknown, never as a value. # # rule_retrieval outcome, when_rules, kept_rules, rules_chars, # stop_reason, input_tokens, output_tokens, duration_ms @@ -190,9 +199,53 @@ # One event per executed tool call; tool arguments and # result content are deliberately absent (ENG-1486). # surface mirrors turn_completed's (ENG-1945). +# root_cause_tier + root_cause_class (WHY it failed, in +# `anton/core/root_cause.py`'s vocabulary — `self_inflicted` +# / `transient` / `external_wall` / `unclassified`, and the +# class within it, which is USUALLY the exception name but +# is a sentinel class where the handler declared the failure +# itself: `empty_code`, `missing_argument`, +# `invalid_argument`, `unknown_resource`, `missing_name`. +# Do NOT join this against Python exception names — nine +# `_SENTINEL_REASONS` map to `self_inflicted` with non- +# exception classes, and `scratchpad_empty_code` is one of +# scratchpad's own, so that join silently drops every +# argument-validation failure. `timeout` is likewise a +# literal, not a type name. `error_type` above only fills in on a +# RAISE; the dominant tool returns a verdict instead, so 83% +# of failures had no cause before ENG-2247. Both are "" on +# success and on an unmigrated handler (`ok="unknown"`). +# Read them as a PAIR with `ok` — a cause only exists where +# the handler said `ok=false`. Same vocabulary as +# turn_completed's `root_cause_*` tally, so where both +# populate they agree; unlike that tally, these are per CALL +# and carry no cumulative-ledger caveat. +# They do NOT reconcile row-for-row, and the gap is +# systematic rather than rare. An `ok="unknown"` call (any +# handler returning a plain string — most of them, see +# ENG-2248) is COUNTED by the turn tally as `unclassified` +# and left BLANK here, because inventing a verdict from +# prose the model can influence is the ENG-1276 defect one +# level up. So `sum(tool rows by tier)` runs systematically +# short of `turn_completed.root_cause_failures`; that is the +# design, not drift. (The inverse also exists but is +# unreachable today: a multimodal `ok=false` result would +# populate here and not in the tally — see +# `_tool_failure_cause`.) # conversation_id + turn_index mirror turn_completed's # values, so a tool row joins to its parent turn row and, # via Langfuse sessionId, to the gateway trace. +# turn_attempt_id also mirrors it, and is the key the join +# should actually use: turn_index alone matched every retry +# of the same turn (18.5% of these rows joined to more than +# one turn row before ENG-2243). +# Not "empty when no books are open" — both emit sites are +# inside the tool loop, so that state is unreachable. The +# divergence that IS reachable: with books closed, +# turn_index falls back to _turn_count + 1 while this would +# be "", so the two join keys would disagree about whether a +# parent turn row exists. Prefer this key and treat an empty +# value as a pre-ENG-2243 build, never as "no parent". # (anton/core/session.py::_emit_tool_completed) # # An event NOT listed here keeps the collector path, so moving one is an @@ -289,8 +342,17 @@ def get_installation_id() -> str: usable as a join key. Returns: - A 16-character hex string (64 bits of entropy), or ``"unknown"`` when - the machine cannot be fingerprinted at all. + A 16-character hex string, or ``"unknown"`` when the machine cannot be + fingerprinted at all. + + 64 bits on the real-MAC path (a sha256 prefix). The no-MAC fallback + persists ``uuid4().hex[:16]``, which is 60: hex position 12 is uuid4's + version nibble, so it is the literal ``4`` in every id that branch ever + writes. Left as-is deliberately — changing the derivation would give + every already-fingerprinted Docker install a NEW ``aid`` and break the + continuity of that identity — but do not repeat the 64-bit claim for + it. Noted while correcting the same error in + ``TurnCost.attempt_id`` (#431 review). """ global _cached_aid if _cached_aid is not None: @@ -328,12 +390,19 @@ def _posthog_body(key: str, action: str, params: dict[str, str]) -> bytes: moment a queued daemon thread got around to sending, and for a cost event that difference is the one you would go on to plot. - Deliberately no ``$insert_id``. The natural key would be - ``(conversation_id, turn_index)``, but an abandoned turn's books and a - later retry can legitimately share both, and dropping that row would lose - exactly the runaway a cancel was investigating (anton#309 review). - ``TurnCost.emitted`` already stops the same books emitting twice, so - dedupe here could only add a way to lose real events. + Deliberately no ``$insert_id`` — but not for the reason this comment + used to give (#431 review). It said the natural key + ``(conversation_id, turn_index)`` was unusable because an abandoned turn's + books and a later retry can legitimately share both. True, and ENG-2243 + then created the key that does not: ``turn_attempt_id`` is unique per turn + EXECUTION, so ``(conversation_id, turn_attempt_id)`` would be a sound + dedupe key. + + The standing reason is the second half: ``TurnCost.emitted`` already stops + the same books emitting twice, one layer earlier and for every sink at + once. So dedupe here would be redundant on the path that matters and could + only add a way to lose real events. Kept out on those grounds, not on the + absence of a key. """ properties = { k: v for k, v in params.items() diff --git a/anton/chat.py b/anton/chat.py index 85f974b6..a959ffd5 100644 --- a/anton/chat.py +++ b/anton/chat.py @@ -339,7 +339,7 @@ async def _handle_connect( global_ws.apply_env_to_process() console.print() - return rebuild_session( + return await rebuild_session( settings=settings, state=state, self_awareness=self_awareness, @@ -1318,7 +1318,19 @@ def run_chat( console: Console, settings: AntonSettings, *, resume: bool = False, first_run: bool = False, desktop_first_run: bool = False ) -> None: """Launch the interactive chat REPL.""" - asyncio.run(_chat_loop(console, settings, resume=resume, first_run=first_run, desktop_first_run=desktop_first_run)) + + async def _main() -> None: + from anton.core.llm.provider import close_live_providers, install_asyncgen_noise_filter + + install_asyncgen_noise_filter() + try: + await _chat_loop(console, settings, resume=resume, first_run=first_run, desktop_first_run=desktop_first_run) + finally: + # Release provider HTTP pools before the loop dies, whatever path + # the loop exited on (see anton/core/llm/provider.py). + await close_live_providers() + + asyncio.run(_main()) async def _chat_loop( @@ -1349,7 +1361,7 @@ async def _chat_loop( edef = dreg.get(conn["engine"]) if edef is not None: register_secret_vars(edef, engine=conn["engine"], name=conn["name"]) - del dv, dreg + del dreg global_memory_dir = Path.home() / ".anton" / "memory" project_memory_dir = settings.workspace_path / ".anton" / "memory" @@ -1407,6 +1419,9 @@ async def _chat_loop( runtime_context=runtime_context, ), workspace=workspace, + # The manager derives each pad's DS_* from this vault; without it a + # pad inherits the whole process env instead. + data_vault=dv, console=console, history_store=history_store, session_id=current_session_id, @@ -1829,7 +1844,7 @@ def _bottom_toolbar(): elif cmd == "/remote": await _handle_remote(console, settings) # Rebuild session so scratchpad uses remote/local factory - session = rebuild_session( + session = await rebuild_session( settings=settings, state=state, self_awareness=self_awareness, @@ -2021,7 +2036,7 @@ def _bottom_toolbar(): from anton.cli import _ensure_api_key _ensure_api_key(settings) - session = rebuild_session( + session = await rebuild_session( settings=settings, state=state, self_awareness=self_awareness, @@ -2104,7 +2119,10 @@ def _bottom_toolbar(): continue except KeyboardInterrupt: pass + finally: + # In a finally, not after the except: an exception escaping the loop + # must still close scratchpads and provider pools on its way out. + await session.close() console.print() console.print("[anton.muted]See you.[/]") - await session.close() diff --git a/anton/chat_session.py b/anton/chat_session.py index 717c35e3..81edfc9b 100644 --- a/anton/chat_session.py +++ b/anton/chat_session.py @@ -46,7 +46,7 @@ def get_runtime_factory(settings: AntonSettings): return local_scratchpad_runtime_factory -def rebuild_session( +async def rebuild_session( *, settings: AntonSettings, state: dict, @@ -62,10 +62,16 @@ def rebuild_session( from anton.core.llm.client import LLMClient from anton.chat import ChatSession from anton.core.llm.tracing import HARNESS_ANTON, SURFACE_CLI + from anton.core.datasources.data_vault import LocalDataVault from anton.core.session import ChatSessionConfig from anton.tools import DEFAULT_SESSION_TOOLS + outgoing = state.get("llm_client") state["llm_client"] = LLMClient.from_settings(settings) + if outgoing is not None: + # Nothing references the outgoing client again; release its provider + # HTTP pools now rather than leaving them open until process exit. + await outgoing.aclose() # Update cortex with new LLM client and memory mode if cortex is not None: @@ -87,6 +93,9 @@ def rebuild_session( runtime_context=runtime_context, ), workspace=workspace, + # Rebuilding drops the old session's manager, so the new one needs the + # vault too or its pads fall back to inheriting the process env. + data_vault=LocalDataVault(), console=console, history_store=history_store, session_id=session_id, diff --git a/anton/cli.py b/anton/cli.py index 0e07ba5a..9a202338 100644 --- a/anton/cli.py +++ b/anton/cli.py @@ -3,6 +3,7 @@ import asyncio import concurrent.futures import importlib +import logging import os import shutil import subprocess @@ -21,6 +22,7 @@ from rich.text import Text from anton import __version__ +from anton.core.llm.provider import close_live_providers, install_asyncgen_noise_filter from anton.utils.prompt import prompt_or_cancel from anton.core.llm.openai import build_chat_completion_kwargs, _is_azure_endpoint @@ -30,6 +32,7 @@ from anton.core.session import ChatSessionConfig from anton.core.llm.client import LLMClient from anton.core.backends.manager import ScratchpadManager +from anton.core.datasources.data_vault import LocalDataVault from anton.commands.datasource import ( handle_remove_data_source, @@ -39,6 +42,23 @@ ) from anton.minds_client import minds_v1_base, resolve_and_probe, test_llm +# The CLI configures no logging, so without a handler on the package logger +# Python's lastResort writes WARNING and above straight to stderr — into the +# middle of the rich Live render. Propagation is untouched, so a host that does +# configure logging (the cowork sidecar, the cloud pod) still emits them. +logging.getLogger("anton").addHandler(logging.NullHandler()) + + +def _run_and_close(coro): + """Run a coroutine, then release provider HTTP pools before the loop dies.""" + async def _wrapped(): + install_asyncgen_noise_filter() + try: + return await coro + finally: + await close_live_providers() + return asyncio.run(_wrapped()) + def _build_scratchpad_manager( settings, @@ -53,6 +73,9 @@ def _build_scratchpad_manager( coding_api_key=coding_conn.api_key or "", coding_base_url=coding_conn.base_url or "", workspace_path=settings.workspace_path, + # Without a vault the manager cannot derive a pad's DS_* and the pad + # falls back to inheriting the whole process env. + data_vault=LocalDataVault(), ) @@ -132,8 +155,9 @@ def _reexec() -> None: # Core dependencies from pyproject.toml that anton needs at runtime _REQUIRED_PACKAGES: dict[str, str] = { - "anthropic": "anthropic>=0.42.0", - "openai": "openai>=2.21.0", + "anthropic": "anthropic>=1.0", + "openai": "openai>=3.0", + "httpx2": "httpx2>=2.7,<3", "pydantic": "pydantic>=2.0", "pydantic_settings": "pydantic-settings>=2.0", "prompt_toolkit": "prompt-toolkit>=3.0", @@ -345,9 +369,9 @@ def _ensure_terms_consent(console: Console, settings) -> None: env_path.parent.mkdir(parents=True, exist_ok=True) # Append if file exists, otherwise create - existing = env_path.read_text() if env_path.is_file() else "" + existing = env_path.read_text(encoding="utf-8") if env_path.is_file() else "" if "ANTON_TERMS_CONSENT" not in existing: - with env_path.open("a") as f: + with env_path.open("a", encoding="utf-8") as f: if existing and not existing.endswith("\n"): f.write("\n") f.write("ANTON_TERMS_CONSENT=true\n") @@ -510,7 +534,7 @@ def _onboard(settings) -> None: ] if sys.stdout.isatty(): - asyncio.run( + _run_and_close( _animate_onboard( console, __version__, _INTRO_LINES, settings=settings, ws=ws ) @@ -1371,7 +1395,7 @@ def _setup_exa(settings, ws) -> None: def _test(): # Sync httpx call — _validate_with_spinner runs us inside a Live. - import httpx as _httpx + import httpx2 as _httpx resp = _httpx.post( "https://api.exa.ai/search", @@ -1424,7 +1448,7 @@ def _setup_brave(settings, ws) -> None: try: def _test(): - import httpx as _httpx + import httpx2 as _httpx resp = _httpx.get( "https://api.search.brave.com/res/v1/web/search", @@ -1652,7 +1676,7 @@ async def _run() -> None: ) await scratchpads.close_all() - asyncio.run(_run()) + _run_and_close(_run()) @app.command("list") @@ -1687,7 +1711,7 @@ async def _run() -> None: ) await scratchpads.close_all() - asyncio.run(_run()) + _run_and_close(_run()) @app.command("remove") @@ -1699,7 +1723,7 @@ def remove_data_source( ) -> None: """Remove a saved connection from the Local Vault.""" - asyncio.run(handle_remove_data_source(console, name)) + _run_and_close(handle_remove_data_source(console, name)) @app.command("test") @@ -1717,7 +1741,7 @@ def test_data_source( scratchpads = _build_scratchpad_manager(settings) async def _run() -> None: - await _handle_test_datasource(console, scratchpads, name) + await handle_test_datasource(console, scratchpads, name) await scratchpads.close_all() - asyncio.run(_run()) + _run_and_close(_run()) diff --git a/anton/commands/datasource/verify.py b/anton/commands/datasource/verify.py index c12fac5e..09ce982d 100644 --- a/anton/commands/datasource/verify.py +++ b/anton/commands/datasource/verify.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -import os import re from typing import TYPE_CHECKING, Awaitable, Callable @@ -11,7 +10,12 @@ from anton.core.backends.base import Cell from anton.core.datasources.data_vault import DataVault, LocalDataVault from anton.core.datasources.datasource_registry import DatasourceEngine, DatasourceField, DatasourceRegistry -from anton.utils.datasources import parse_connection_slug, register_secret_vars, restore_namespaced_env +from anton.utils.datasources import ( + parse_connection_slug, + register_secret_vars, + restore_namespaced_env, + set_ds_env_values, +) from anton.utils.prompt import prompt_or_cancel if TYPE_CHECKING: @@ -36,7 +40,7 @@ async def run_connection_test( *, interactive: bool = True, ) -> bool: - """Inject flat DS_* vars, run engine_def.test_snippet, restore env. + """Run engine_def.test_snippet with flat DS_* vars in the pad's own env. Returns True on success, False if the user declines retry after failure. Mutates credentials in-place when the user re-enters secrets on retry. @@ -49,14 +53,13 @@ async def run_connection_test( console.print() console.print("[anton.cyan](anton)[/] Got it. Testing connection…") - vault.clear_ds_env() - flat_ds_env: dict[str, str] = {} - for key, value in credentials.items(): - if key.startswith("_"): - continue - os.environ[f"DS_{key.upper()}"] = value - flat_ds_env[f"DS_{key.upper()}"] = value + flat_ds_env: dict[str, str] = { + f"DS_{key.upper()}": value + for key, value in credentials.items() + if not key.startswith("_") + } register_secret_vars(engine_def) # flat mode, for scrubbing during test + set_ds_env_values(flat_ds_env) try: pad = await scratchpads.get_or_create( @@ -198,10 +201,9 @@ async def handle_test_datasource( f"[anton.cyan](anton)[/] Testing connection [bold]{slug}[/bold]…" ) - vault.clear_ds_env() - vault.inject_env(engine, name, flat=True) register_secret_vars(engine_def) # flat names for scrubbing during test flat_ds_env = vault.env_for(engine, name, flat=True) or {} + set_ds_env_values(flat_ds_env) cell = None try: diff --git a/anton/commands/session.py b/anton/commands/session.py index a2526b8b..b36310aa 100644 --- a/anton/commands/session.py +++ b/anton/commands/session.py @@ -110,7 +110,7 @@ async def restore_session( await session._scratchpads.close_all() # Build new session with restored history - new_session = rebuild_session( + new_session = await rebuild_session( settings=settings, state=state, self_awareness=self_awareness, diff --git a/anton/commands/setup.py b/anton/commands/setup.py index e3c2b222..64c55638 100644 --- a/anton/commands/setup.py +++ b/anton/commands/setup.py @@ -95,7 +95,7 @@ def _print_choices(): console.print("[anton.success]Configuration updated.[/]") console.print() - return rebuild_session( + return await rebuild_session( settings=settings, state=state, self_awareness=self_awareness, diff --git a/anton/core/artifacts/backend_launcher.py b/anton/core/artifacts/backend_launcher.py index bd2138ff..074a00f3 100644 --- a/anton/core/artifacts/backend_launcher.py +++ b/anton/core/artifacts/backend_launcher.py @@ -57,9 +57,23 @@ def _anton_state_pythonpath_dir() -> str: return str(root) -def _build_backend_env(extra_env: dict[str, str] | None) -> dict[str, str]: - """Subprocess env: inherited environ + extra_env, with anton_state on PYTHONPATH.""" - env = {**os.environ, **(extra_env or {})} +def _build_backend_env( + extra_env: dict[str, str] | None, + ds_env: dict[str, str] | None = None, +) -> dict[str, str]: + """Subprocess env: inherited environ + extra_env, with anton_state on PYTHONPATH. + + A non-None `ds_env` replaces the inherited DS_* entirely, so the backend + sees only the datasources it declared. + """ + env = {**os.environ} + # Before the strip, so a caller's DS_* survive only when ds_env is None; + # a project .env cannot add one to a backend that declared its own. + env.update(extra_env or {}) + if ds_env is not None: + for key in [k for k in env if k.startswith("DS_")]: + del env[key] + env.update(ds_env) isolated = _anton_state_pythonpath_dir() existing = env.get("PYTHONPATH", "") env["PYTHONPATH"] = isolated + (os.pathsep + existing if existing else "") @@ -90,6 +104,7 @@ async def launch_artifact_backend( path: str = "backend.py", extra_args: list[str] | None = None, extra_env: dict[str, str] | None = None, + ds_env: dict[str, str] | None = None, health_path: str = "/", health_timeout: float = 10.0, ) -> dict | str: @@ -109,6 +124,11 @@ async def launch_artifact_backend( `extra_env` is merged over the inherited `os.environ` for the spawned process only (e.g. datasource `DS_*` secrets) — it never mutates the parent's environment, keeping secrets scoped to the backend subprocess. + + `ds_env`, when given, is the backend's complete `DS_*` set: the inherited + ones are dropped first, so a connection the artifact did not declare (or + one a concurrent turn injected) cannot reach it. Callers that still route + `DS_*` through `extra_env` keep the old merge-only behaviour. """ extra_args = list(extra_args or []) folder = artifact_folder @@ -221,7 +241,7 @@ def _set_pdeathsig() -> None: stderr=log_fd, stdin=asyncio.subprocess.DEVNULL, preexec_fn=preexec_fn, - env=_build_backend_env(extra_env), + env=_build_backend_env(extra_env, ds_env), ) except OSError as exc: log_fd.close() diff --git a/anton/core/backends/base.py b/anton/core/backends/base.py index 6436ca42..1eb69c9b 100644 --- a/anton/core/backends/base.py +++ b/anton/core/backends/base.py @@ -269,4 +269,7 @@ def __call__( # Explicit DS_* env values for this pad, when the host has a data vault # to scope them from. Optional for hosts/test doubles that predate it. scratchpad_ds_env: dict[str, str] | None = None, - ) -> ScratchpadRuntime: ... \ No newline at end of file + # Explicit workspace .env values for this pad. Optional for hosts + # and test doubles that predate it. + workspace_env_overlay: dict[str, str] | None = None, + ) -> ScratchpadRuntime: ... diff --git a/anton/core/backends/local.py b/anton/core/backends/local.py index ad4af638..b53eb160 100644 --- a/anton/core/backends/local.py +++ b/anton/core/backends/local.py @@ -205,6 +205,7 @@ def __init__( workspace_path: Path | None = None, session_id: str | None = None, scratchpad_ds_env: dict[str, str] | None = None, + workspace_env_overlay: dict[str, str] | None = None, _venvs_base: Path | None = None, ) -> None: super().__init__( @@ -230,6 +231,7 @@ def __init__( self._session_id: str | None = session_id # DS_* overlay for this pad's subprocess; None keeps legacy full-copy behaviour. self._scratchpad_ds_env: dict[str, str] | None = scratchpad_ds_env + self._workspace_env_overlay: dict[str, str] | None = workspace_env_overlay self._proc: asyncio.subprocess.Process | None = None self._boot_path: str | None = None self._venv_dir: str | None = None @@ -606,6 +608,10 @@ async def start(self) -> None: # Force UTF-8 in the child (ENG-824). env = _utf8_env(os.environ) + # Only-if-unset, as apply_env_to_process was, and before the DS_* strip + # so a project .env can neither replace PATH nor smuggle a credential. + for key, value in (self._workspace_env_overlay or {}).items(): + env.setdefault(key, value) if self._scratchpad_ds_env is not None: # Never trust inherited DS_* values — strip them, then overlay # exactly what this pad should see. @@ -1358,6 +1364,7 @@ def local_scratchpad_runtime_factory( workspace_path: Path | None, session_id: str | None = None, scratchpad_ds_env: dict[str, str] | None = None, + workspace_env_overlay: dict[str, str] | None = None, ) -> ScratchpadRuntime: return LocalScratchpadRuntime( name=name, @@ -1369,4 +1376,5 @@ def local_scratchpad_runtime_factory( workspace_path=workspace_path, session_id=session_id, scratchpad_ds_env=scratchpad_ds_env, + workspace_env_overlay=workspace_env_overlay, ) diff --git a/anton/core/backends/manager.py b/anton/core/backends/manager.py index 8cff4c0b..d0db60e4 100644 --- a/anton/core/backends/manager.py +++ b/anton/core/backends/manager.py @@ -26,6 +26,7 @@ def __init__( workspace_path: Path | None = None, session_id: str | None = None, data_vault: DataVault | None = None, + workspace_env_overlay: dict[str, str] | None = None, ) -> None: self._pads: dict[str, ScratchpadRuntime] = {} self._runtime_factory = runtime_factory @@ -39,6 +40,7 @@ def __init__( # scoped per conversation (ENG-1124). self._session_id = session_id self._data_vault = data_vault + self._workspace_env_overlay = workspace_env_overlay # Only pass `session_id` to factories that accept it. A default on the Protocol # does not adapt an existing callable, so passing it unconditionally raises # `TypeError: unexpected keyword argument` for an out-of-tree factory written @@ -50,6 +52,9 @@ def __init__( self._factory_takes_scratchpad_ds_env = self._probe_factory_kwarg( runtime_factory, "scratchpad_ds_env" ) + self._factory_takes_workspace_env_overlay = self._probe_factory_kwarg( + runtime_factory, "workspace_env_overlay" + ) self._available_packages: list[str] = self.probe_packages() @staticmethod @@ -220,6 +225,11 @@ async def get_or_create( if self._factory_takes_scratchpad_ds_env else {} ), + **( + {"workspace_env_overlay": self._workspace_env_overlay} + if self._factory_takes_workspace_env_overlay + else {} + ), ) await pad.start() self._pads[name] = pad diff --git a/anton/core/backends/remote.py b/anton/core/backends/remote.py index a747fddd..23bef18d 100644 --- a/anton/core/backends/remote.py +++ b/anton/core/backends/remote.py @@ -12,6 +12,8 @@ from pathlib import Path from typing import AsyncIterator +import httpx2 as httpx + from anton.core.backends.base import Cell, ScratchpadRuntime @@ -56,47 +58,33 @@ def _headers(self) -> dict[str, str]: async def _post(self, path: str, body: dict | None = None) -> dict: """POST to the remote service and return parsed JSON.""" - import aiohttp - url = f"{self._endpoint_url}{path}" - async with aiohttp.ClientSession() as session: - async with session.post( - url, json=body or {}, headers=self._headers(), timeout=aiohttp.ClientTimeout(total=300) - ) as resp: - if resp.status >= 400: - text = await resp.text() - raise RuntimeError(f"Remote scratchpad error ({resp.status}): {text}") - return await resp.json() + async with httpx.AsyncClient(timeout=300) as client: + resp = await client.post(url, json=body or {}, headers=self._headers()) + if resp.status_code >= 400: + raise RuntimeError(f"Remote scratchpad error ({resp.status_code}): {resp.text}") + return resp.json() async def _get(self, path: str, params: dict | None = None) -> dict: """GET from the remote service and return parsed JSON.""" - import aiohttp - url = f"{self._endpoint_url}{path}" - async with aiohttp.ClientSession() as session: - async with session.get( - url, params=params, headers=self._headers(), timeout=aiohttp.ClientTimeout(total=30) - ) as resp: - if resp.status >= 400: - text = await resp.text() - raise RuntimeError(f"Remote scratchpad error ({resp.status}): {text}") - return await resp.json() + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url, params=params, headers=self._headers()) + if resp.status_code >= 400: + raise RuntimeError(f"Remote scratchpad error ({resp.status_code}): {resp.text}") + return resp.json() async def _sse(self, path: str, body: dict) -> AsyncIterator[dict]: """POST to an SSE endpoint and yield parsed events.""" - import aiohttp - url = f"{self._endpoint_url}{path}" - async with aiohttp.ClientSession() as session: - async with session.post( - url, json=body, headers=self._headers(), timeout=aiohttp.ClientTimeout(total=600) - ) as resp: - if resp.status >= 400: - text = await resp.text() - raise RuntimeError(f"Remote scratchpad error ({resp.status}): {text}") + async with httpx.AsyncClient(timeout=600) as client: + async with client.stream("POST", url, json=body, headers=self._headers()) as resp: + if resp.status_code >= 400: + text = (await resp.aread()).decode("utf-8", errors="replace") + raise RuntimeError(f"Remote scratchpad error ({resp.status_code}): {text}") buffer = "" - async for chunk in resp.content: + async for chunk in resp.aiter_bytes(): buffer += chunk.decode("utf-8", errors="replace") while "\n\n" in buffer: event_str, buffer = buffer.split("\n\n", 1) @@ -236,17 +224,12 @@ async def _resolve_endpoint(self, endpoint_url: str) -> str: Calls /resolve on the Cloudflare Worker which returns the instance's direct IP. Caches the result for subsequent calls. """ - import aiohttp - url = f"{endpoint_url}/resolve" - async with aiohttp.ClientSession() as session: - async with session.get( - url, headers=self._headers(), timeout=aiohttp.ClientTimeout(total=15) - ) as resp: - if resp.status >= 400: - text = await resp.text() - raise RuntimeError(f"Failed to resolve remote scratchpad ({resp.status}): {text}") - data = await resp.json() + async with httpx.AsyncClient(timeout=15) as client: + resp = await client.get(url, headers=self._headers()) + if resp.status_code >= 400: + raise RuntimeError(f"Failed to resolve remote scratchpad ({resp.status_code}): {resp.text}") + data = resp.json() endpoint = data.get("endpoint", "") if not endpoint: diff --git a/anton/core/datasources/data_vault.py b/anton/core/datasources/data_vault.py index 6172b155..79513d9e 100644 --- a/anton/core/datasources/data_vault.py +++ b/anton/core/datasources/data_vault.py @@ -420,7 +420,7 @@ class TurnKeyDataVault: matching the fact that a cloud turn never edits connections mid-turn. """ - def __init__(self, oauth: dict[str, Any], *, base_url: str | None = None) -> None: + def __init__(self, oauth: dict[str, Any]) -> None: self._turn_key = str(oauth.get("turn_key") or "") self._connections: list[dict[str, str]] = [ {"engine": str(c["engine"]), "name": str(c["name"])} @@ -428,10 +428,18 @@ def __init__(self, oauth: dict[str, Any], *, base_url: str | None = None) -> Non if isinstance(c, dict) and c.get("engine") and c.get("name") ] self._connection_keys = frozenset((c["engine"], c["name"]) for c in self._connections) + # ENG-2128: this used to also accept a keyword-only `base_url` + # override, but the one real call site (cloud_turn/session.py) + # constructs positionally and never bound it, so a value + # cowork-server put in the oauth block's own `base_url` field was + # dead on the wire - the module comment on + # ANTON_CLOUD_AUTH_BASE_URL_ENV above already states the intended + # design ("never taken from the wire request"), which the removed + # kwarg contradicted by existing at all. Removed rather than wired + # up: the env var is already the correct, working, per-environment + # source, and nothing needs cowork-server to steer this per-request. self._base_url = ( - base_url - or os.environ.get(ANTON_CLOUD_AUTH_BASE_URL_ENV) - or _DEFAULT_AUTH_BASE_URL + os.environ.get(ANTON_CLOUD_AUTH_BASE_URL_ENV) or _DEFAULT_AUTH_BASE_URL ).rstrip("/") # Per-turn cache: the loop in restore_namespaced_env() calls # inject_env() once per connection already, but read_record()/load() diff --git a/anton/core/llm/anthropic.py b/anton/core/llm/anthropic.py index 72654c59..38b4ff66 100644 --- a/anton/core/llm/anthropic.py +++ b/anton/core/llm/anthropic.py @@ -8,7 +8,7 @@ from anton.utils.datasources import scrub_credentials -from .provider import safe_parse_tool_input +from .provider import register_provider, safe_parse_tool_input, unregister_provider from .provider import ( ContextOverflowError, LLMProvider, @@ -193,6 +193,12 @@ def _build_native_web_tools( class AnthropicProvider(LLMProvider): name: str = "anthropic" + async def aclose(self) -> None: + client = getattr(self, "_client", None) + if client is not None: + await client.close() + unregister_provider(self) + def native_web_tools(self) -> set[str]: # Anthropic's Messages API ships both server-side web_search and # web_fetch tools; we route both through the provider when enabled. @@ -213,6 +219,7 @@ def __init__( if api_key: kwargs["api_key"] = api_key self._client = anthropic.AsyncAnthropic(**kwargs) + register_provider(self) def export_connection_info(self) -> ProviderConnectionInfo: return ProviderConnectionInfo(provider=self.name, api_key=self._api_key) diff --git a/anton/core/llm/client.py b/anton/core/llm/client.py index 7d2e09fe..228285e4 100644 --- a/anton/core/llm/client.py +++ b/anton/core/llm/client.py @@ -143,6 +143,18 @@ def __init__( # swallowed (see _notify_usage). self.usage_listener = None # Callable[[str, str, Usage], None] | None + async def aclose(self) -> None: + """Close provider transports. The three roles may share objects.""" + seen: set[int] = set() + for p in (self._planning_provider, self._coding_provider, self._router_provider): + if p is None or id(p) in seen: + continue + seen.add(id(p)) + try: + await p.aclose() + except Exception: + pass # a cleanup-only failure must not break shutdown; cancellation propagates + def _notify_usage(self, role: str, model: str, usage, listener=None) -> None: """Report one call's usage to the turn-cost accumulator. diff --git a/anton/core/llm/openai.py b/anton/core/llm/openai.py index c5357e47..75dff37e 100644 --- a/anton/core/llm/openai.py +++ b/anton/core/llm/openai.py @@ -1,5 +1,6 @@ from __future__ import annotations +import inspect import json import logging import os @@ -7,11 +8,13 @@ from typing import NoReturn import openai +from contextlib import aclosing + from openai import AsyncAzureOpenAI from anton.utils.datasources import scrub_credentials -from .provider import safe_parse_tool_input +from .provider import register_provider, safe_parse_tool_input, unregister_provider from .provider import ( ContentValidationError, ContextOverflowError, @@ -809,6 +812,28 @@ def _as_int(value) -> int: return total - read - write, read, write + +async def _aclose_stream(stream: object) -> None: + """Release a provider stream and its HTTP pool connection. + + Cleanup-only failures are swallowed so they cannot replace the primary + exception; cancellation propagates. + """ + if stream is None: + return + # Prefer aclose (plain async generators); SDK streams expose close(). The + # order matters: an object with a sync close() beside an async aclose() + # must not short-circuit onto the sync one and leak with no diagnostic. + closer = getattr(stream, "aclose", None) or getattr(stream, "close", None) + if closer is not None: + try: + result = closer() + if inspect.isawaitable(result): + await result + except Exception: + pass + + class OpenAIProvider(LLMProvider): name: str = "openai" @@ -819,6 +844,12 @@ class OpenAIProvider(LLMProvider): FLAVOR_MINDS_PASSTHROUGH = "minds-passthrough" # mdb.ai — chat.completions w/ native tools. FLAVOR_OPENAI_COMPATIBLE_GENERIC = "openai-compatible-generic" # third-party. + async def aclose(self) -> None: + client = getattr(self, "_client", None) + if client is not None: + await client.close() + unregister_provider(self) + def __init__( self, api_key: str | None = None, @@ -865,7 +896,7 @@ def __init__( }: self._emit_trace_headers = True - import httpx + import httpx2 as httpx client_api_key = api_key_provider if api_key_provider is not None else api_key if api_version and _is_azure_endpoint(base_url): @@ -907,6 +938,10 @@ def __init__( kwargs["http_client"] = httpx.AsyncClient(verify=False) self._client = openai.AsyncOpenAI(**kwargs) + # After the if/else, not per branch: a future client flavor must not be + # able to skip registration and leave its pool unclosed. + register_provider(self) + def export_connection_info(self) -> ProviderConnectionInfo: return ProviderConnectionInfo( provider=self.name, @@ -1140,15 +1175,19 @@ async def stream( native_web_tools: set[str] | None = None, ) -> AsyncIterator[StreamEvent]: if self._flavor == self.FLAVOR_OPENAI: - async for event in self._stream_via_responses( + # aclosing, not a bare `async for`: if the consumer abandons us the + # inner generator closes now rather than at asyncgen finalization. + inner = self._stream_via_responses( model=model, system=system, messages=messages, tools=tools, max_tokens=max_tokens, native_web_tools=native_web_tools, - ): - yield event + ) + async with aclosing(inner): + async for event in inner: + yield event return oai_messages = _translate_messages(system, messages, supports_vision=self._supports_vision, vision_format=self._vision_format) @@ -1190,6 +1229,7 @@ async def stream( # BEFORE this (request establishment) was already SDK-retried → fail fast. stream_started = False + stream = None try: stream = await self._client.chat.completions.create(**kwargs) stream_started = True @@ -1335,6 +1375,9 @@ async def stream( session_backoff=True, model=model, ) from exc + finally: + await _aclose_stream(stream) + # Finalize tool calls. Same safe-parse protection as the # non-streaming path — a model cut off mid-JSON-arguments # would otherwise crash the whole turn here with an opaque @@ -1521,6 +1564,7 @@ async def _stream_via_responses( # a failure after this is mid-stream and was never SDK-retried (ENG-673). stream_started = False + stream = None try: stream = await self._client.responses.create(**kwargs) stream_started = True @@ -1676,6 +1720,9 @@ async def _stream_via_responses( session_backoff=True, model=model, ) from exc + finally: + await _aclose_stream(stream) + yield StreamComplete( response=LLMResponse( content=content_text, diff --git a/anton/core/llm/provider.py b/anton/core/llm/provider.py index 5cca667f..a68f8b13 100644 --- a/anton/core/llm/provider.py +++ b/anton/core/llm/provider.py @@ -1,5 +1,6 @@ from __future__ import annotations +import weakref from abc import ABC, abstractmethod from collections.abc import AsyncIterator from dataclasses import dataclass, field @@ -9,6 +10,78 @@ from anton.core.interaction.elicit import AskAnswer, AskRequest +# Providers hold an HTTP pool. Unclosed, httpx2 prints a traceback when the +# event loop finalizes its async generators, so entry points that own the loop +# drain this registry before it dies. Weak refs: long-lived hosts that consume +# anton as a library (cowork-server) build providers per turn and never drain, +# so strong refs would pin every provider and its pool for the process +# lifetime. A provider collected before the drain hands its pool to the GC — +# the pre-registry behavior, and silent while the loop is still running. +_LIVE_PROVIDERS: weakref.WeakSet[LLMProvider] = weakref.WeakSet() + + +def register_provider(provider: LLMProvider) -> None: + _LIVE_PROVIDERS.add(provider) + + +def unregister_provider(provider: LLMProvider) -> None: + """Drop a closed provider so the exit drain does not close it twice.""" + _LIVE_PROVIDERS.discard(provider) + + +async def close_live_providers() -> None: + providers = list(_LIVE_PROVIDERS) + _LIVE_PROVIDERS.clear() + for provider in providers: + try: + await provider.aclose() + except Exception: + pass # a cleanup-only failure must not break shutdown; cancellation propagates + + +def _is_upstream_asyncgen_noise(context: dict) -> bool: + """One known upstream error, matched narrowly. + + httpx2 abandons httpcore2's byte-stream ``__aiter__`` generators + (``PoolByteStream``, ``HTTP11ConnectionByteStream``, ...) when a response + is closed mid-body — every SSE stream ends this way — and at loop shutdown + the ``athrow(GeneratorExit)`` trips httpcore2's ``safe_async_iterate``: + RuntimeError("generator didn't stop after athrow()"). Present through + httpcore2 2.12; harmless — the pool is already closed — but it prints a + traceback on every clean exit. Matched by the generator's source file, not + its class name: which stream class is left abandoned varies run to run. + """ + exc = context.get("exception") + agen = context.get("asyncgen") + code = getattr(agen, "ag_code", None) + return ( + isinstance(exc, RuntimeError) + and "didn't stop after athrow" in str(exc) + and "httpcore2" in getattr(code, "co_filename", "") + ) + + +def install_asyncgen_noise_filter() -> None: + """Silence the upstream error above on the running loop. + + Everything else still reaches the default handler. Callers that own their + event loop (CLI entry points) install this; a host with its own exception + handler is left alone. + """ + import asyncio + + loop = asyncio.get_running_loop() + if loop.get_exception_handler() is not None: + return + + def _handler(loop, context): + if _is_upstream_asyncgen_noise(context): + return + loop.default_exception_handler(context) + + loop.set_exception_handler(_handler) + + @dataclass class ToolCall: id: str @@ -511,9 +584,11 @@ class StructuredOutputError(ValueError): otherwise look identical in a log and pull in opposite directions: - ``False`` — the model narrated in plain ``content`` and never got to the - call (the narrating aliases — ``mindshub_air``/``kimi``, ``deepseek``, - ``qwen`` — do this deterministically under a tight budget, ENG-1081). - The cure is a bigger budget. + call. Measured on the MindsHub aliases in 2026-07, when ``mindshub_air`` + served Kimi K2.6: they did this deterministically under a tight budget + (ENG-1081). Those aliases have since been repointed and none of them + narrates now (ENG-1687), so treat this branch as covering BYOK and local + models rather than any particular alias. The cure is a bigger budget. - ``True`` — the call started and the budget ran out inside its JSON arguments. A bigger budget only helps until the payload grows again; the cure is a smaller response (ENG-1523). @@ -563,12 +638,20 @@ class TransientProviderError(ConnectionError): def __init__( self, message: str, *, provider: str = "", code: str | None = None, retry_after: float | None = None, session_backoff: bool = True, - model: str = "", + model: str = "", status_code: int | None = None, ) -> None: super().__init__(message) self.provider = provider self.code = code self.retry_after = retry_after + # The HTTP status this was classified FROM, when there was one + # (ENG-1361). `code` cannot carry it: a downstream conversion to + # ProviderOverloadedError needs `code` for the card vocabulary, so the + # originating status would otherwise be lost before it reaches + # telemetry. None for a mid-stream failure (the status was 200) and for + # a connection error (no response at all) — those are genuinely absent, + # not unknown. + self.status_code = status_code # The model that was in flight when this failed — so a downstream # ProviderOverloadedError names the ACTUAL model (planning OR coding), # not whatever the session defaults to (ENG-673). @@ -853,11 +936,13 @@ def classify_transient( return TransientProviderError( f"{provider or 'The model provider'} is momentarily overloaded.", provider=provider, code=etype, session_backoff=session_backoff, model=model, + status_code=status_code, ) if isinstance(status_code, int) and 500 <= status_code < 600: return TransientProviderError( f"{provider or 'The model provider'} returned {status_code}.", provider=provider, code=f"http_{status_code}", session_backoff=False, model=model, + status_code=status_code, ) if status_code == 429 and not b.get("detail"): # Plain rate-limit ("slow down"), NOT an out-of-quota 429. Quota 429s are @@ -893,7 +978,7 @@ def classify_transient( provider=provider, code="rate_limited", session_backoff=velocity_confirmed, retry_after=retry_after if velocity_confirmed else None, - model=model, + model=model, status_code=status_code, ) return None @@ -1050,6 +1135,100 @@ class EndpointConfigurationError(ConnectionError): """ +# --------------------------------------------------------------------------- # +# Curated failures + the analytics vocabulary for them (ENG-1361) +# --------------------------------------------------------------------------- # + +# Every CURATED failure: a typed exception carrying user-ready copy that a host +# maps to an actionable card. These must FAIL a turn, never be wrapped into +# assistant prose — the card can only fire when the exception propagates, and +# the generic "please try again or rephrase your request" fallback is actively +# wrong for all of them (rephrasing cannot fix an outage, a dead credential, or +# an empty wallet). +# +# Listed HERE, beside the class definitions, rather than at the consumer in +# `session.py`: the previous allowlist lived next to the `except` that used it, +# and drifted three times — each omission found by a user hitting it in +# production rather than by the suite (ENG-1361, ENG-1310). Adding a class now +# means ignoring the comment directly above it, and `test_curated_errors.py` +# fails on any exception class defined in this module that is neither listed +# here nor deliberately excluded. +# +# NOT a marker base class on purpose: a mixin would change the MRO of every one +# of these across a version-skew boundary that cowork-server already handles +# defensively (it imports each type lazily precisely because anton's version +# floats underneath it). High risk, and the module-walk test gives the same +# guarantee without touching the hierarchy. +# +# Builtin `ConnectionError` is deliberately ABSENT even though the status +# mapper's terminal catch-all raises one: it is the base class of half this +# tuple and of unrelated socket failures, so listing it would silently curate +# everything. Giving that catch-all its own type is ENG-1283. +CURATED_PROVIDER_ERRORS: tuple[type[BaseException], ...] = ( + ContextOverflowError, + TokenLimitExceeded, + ProviderAuthError, + StructuredOutputError, + TransientProviderError, + ProviderOverloadedError, + ModelUnavailableError, + ContentValidationError, + EndpointConfigurationError, +) + + +# The analytics vocabulary for WHY the provider failed, kept deliberately small +# and closed (ENG-1361). `code` on the exception cannot serve this purpose: it +# selects the client's error card (`provider_overloaded` / `rate_limited` are +# matched literally in ChatView.jsx), so it is a wire contract with the +# renderer, not a free label. And the raw codes mix cardinalities — `http_503` +# next to `connection_error` — which would spread one failure mode across many +# analytics rows. The HTTP status, when there is one, rides in its own field. +PROVIDER_FAILURE_KINDS: frozenset[str] = frozenset({ + "overload_signal", # the provider SAID it was overloaded / erroring + "rate_limit", # velocity 429 — waiting is the remedy + "http_5xx", # request-time 5xx status + "connection_failure", # never reached it, or the connection dropped + "bad_response", # a 200 whose body was unusable +}) + +# Codes that mean "we got a 200 and the body was unusable": an unclassifiable +# mid-stream error event, a stream that stopped early, or one that never +# started. Distinct from `overload_signal` because the provider told us +# nothing about why — claiming overload here would over-report incidents. +_BAD_RESPONSE_CODES = frozenset({"stream_error", "truncated_stream", "empty_response"}) + + +def provider_failure_kind(code: str | None) -> str: + """Map a `TransientProviderError.code` to `PROVIDER_FAILURE_KINDS`. + + Returns "" for anything unrecognised rather than guessing — an empty value + in analytics is a prompt to extend the vocabulary, whereas a wrong one is + invisible. Every code anton currently mints is covered; the exhaustiveness + check lives in `test_curated_errors.py`. + """ + if not code: + return "" + if code in _TRANSIENT_ERROR_TYPES: + return "overload_signal" + if code in _BAD_RESPONSE_CODES: + return "bad_response" + if code == "rate_limited": + return "rate_limit" + if code == "connection_error": + return "connection_failure" + if code.startswith("http_"): + # Only 5xx is minted with this prefix (`classify_transient`), but parse + # rather than trust it: a future 4xx would otherwise be mislabelled as a + # server fault, which is the one direction that misleads an operator. + try: + status = int(code[len("http_"):]) + except ValueError: + return "" + return "http_5xx" if 500 <= status < 600 else "" + return "" + + @dataclass class ProviderConnectionInfo: """Serializable provider connection details. @@ -1065,6 +1244,10 @@ class ProviderConnectionInfo: class LLMProvider(ABC): + + async def aclose(self) -> None: + """Release transport resources. No-op unless the provider holds a client.""" + return None # Human-readable provider id (e.g. "anthropic", "openai-compatible"). name: str = "" diff --git a/anton/core/llm/structured.py b/anton/core/llm/structured.py index 37728d2e..6e011d04 100644 --- a/anton/core/llm/structured.py +++ b/anton/core/llm/structured.py @@ -37,12 +37,15 @@ # Default output-budget ladder for forced-schema calls: first attempt, then # one retry used only when the first came back truncated. Sized by the same -# measurement as the completion verifier's (ENG-1081): narrating models -# (`mindshub_air`/kimi, `deepseek`) spend 245–1,654+ tokens on prose before -# reaching the forced tool call — and the narration scales with the input, so -# a consolidation pass over a whole scratchpad session was observed filling -# 2,048 exactly (ENG-1084). Non-narrating models answer these calls in tens -# of tokens and never pay for the headroom. +# measurement as the completion verifier's (ENG-1081): as measured in 2026-07, +# narrating models (`mindshub_air` — then Kimi K2.6 — `kimi`, `deepseek`) spent +# 245–1,654+ tokens on prose before reaching the forced tool call, and the +# narration scaled with the input, so a consolidation pass over a whole +# scratchpad session was observed filling 2,048 exactly (ENG-1084). +# `mindshub_air` was repointed to a GPT model around 2026-08-10 and no MindsHub +# alias narrates today (ENG-1687) — the ladder is kept for the unmeasured BYOK +# and local models anton also runs on. Non-narrating models answer these calls +# in tens of tokens and never pay for the headroom. DEFAULT_STRUCTURED_BUDGETS: tuple[int, ...] = (2048, 4096) diff --git a/anton/core/memory/consolidator.py b/anton/core/memory/consolidator.py index 75cdbb0e..cd3a7185 100644 --- a/anton/core/memory/consolidator.py +++ b/anton/core/memory/consolidator.py @@ -173,8 +173,10 @@ async def replay_and_extract( try: # Consolidation feeds in a whole scratchpad session, and narration - # scales with input — observed filling 2,048 exactly in prod on - # `mindshub_air` (ENG-1084). The ladder's 4,096 retry covers it. + # scales with input — observed filling 2,048 exactly in prod in + # 2026-07, on `mindshub_air` while it still served Kimi K2.6 + # (ENG-1084; that alias is a GPT model now, ENG-1687). The ladder's + # 4,096 retry covers it, and still has to for BYOK models. result: _ConsolidatedLessons = await generate_with_truncation_retry( llm_client.generate_object_code, _ConsolidatedLessons, diff --git a/anton/core/memory/cortex.py b/anton/core/memory/cortex.py index 092d2089..893a9a9d 100644 --- a/anton/core/memory/cortex.py +++ b/anton/core/memory/cortex.py @@ -695,8 +695,9 @@ async def maybe_update_identity(self, user_message: str) -> None: try: # 512 sat inside the measured narration range (245–1,654+), so # narrating models truncated on essentially every pass and the - # silent except below hid it — confirmed live in prod on - # `mindshub_air` (ENG-1084). + # silent except below hid it — confirmed live in prod in 2026-07 on + # `mindshub_air`, which served Kimi K2.6 then and serves a GPT model + # now (ENG-1084, ENG-1687). result: _IdentityFacts = await generate_with_truncation_retry( self._llm.generate_object_code, _IdentityFacts, diff --git a/anton/core/root_cause.py b/anton/core/root_cause.py index 6731d494..a2f6929b 100644 --- a/anton/core/root_cause.py +++ b/anton/core/root_cause.py @@ -69,6 +69,23 @@ #: scores 1 on both rungs and is invisible. _EXACT_ONLY_CLASSES = frozenset({"missing_file"}) +#: Classes `classify` returns as literals rather than through one of the tables +#: below — the runtime WORDS these failures instead of raising a mapped type, +#: so there is no exception name to look up. Named rather than inlined so +#: `ALL_CLASSES` is a real derivation: a consumer enumerating the vocabulary +#: from the tables alone silently misses them. +#: +#: `timeout` is the one that bites. It is reachable in production on every +#: scratchpad cell timeout (`backends/local.py` builds "Cell timed out after +#: {N}s total", which arrives here as the tool's `reason`) and appears in no +#: table, so a closed-set guard built from the tables reports a legitimate +#: value as a novel class. `permission_denied` and `connection_refused` were +#: covered only by coincidence — they happen to also be `_WALL_TYPES` values. +CLS_UNCLASSIFIED = "unclassified" +CLS_TIMEOUT = "timeout" +CLS_PERMISSION_DENIED = "permission_denied" +CLS_CONNECTION_REFUSED = "connection_refused" + #: The agent's own bugs. It can fix these by writing better code, so repetition #: is iteration, not a wall — however many times it repeats. _SELF_INFLICTED = frozenset({ @@ -125,17 +142,41 @@ # A refused package spec (flag/URL/path-shaped entry) is the agent's own # bad argument, not an environment wall (ENG-1635). "package_install_rejected": (TIER_SELF, "invalid_argument"), + # `read_image`, ENG-2248: both are the agent's own file choice, and both + # messages tell it what to do instead (use a real image / resize). + "not_an_image": (TIER_SELF, "invalid_argument"), + "image_too_large": (TIER_SELF, "invalid_argument"), # The agent named something that does not exist. Ambiguous — it could be a # genuinely absent resource — but the agent chose the identifier and can # list the real ones, so it resolves to the non-tripping side. "artifact_not_found": (TIER_SELF, "unknown_resource"), "unknown_datasource": (TIER_SELF, "unknown_resource"), + # Same shape for a path: `read_image` reports the file the agent named as + # absent. Genuinely absent files exist, but the agent supplied the path and + # can list the directory, so it resolves to the non-tripping side. + # + # NOT named `missing_file`, though that is what it describes (#435 review). + # `missing_file` is already a CLASS in this module, reached by a different + # path — `_WALL_TYPES` maps FileNotFoundError to it, `_STATUS_WALLS` maps + # 404 to it, it is the sole member of `_EXACT_ONLY_CLASSES`, and it gets + # path-identifier extraction below. A sentinel KEY of the same name that + # resolves to a DIFFERENT class is the kind of collision a future editor + # reads straight past. + "path_not_found": (TIER_SELF, "unknown_resource"), # Genuine walls: the environment is missing something the agent cannot add. "package_install_failed": (TIER_WALL, "missing_dependency"), "store_unavailable": (TIER_WALL, "service_unavailable"), # Could be environment or config and the sentinel does not say which, so it # stays out of every trip rung until something distinguishes them. "launch_failed": (TIER_UNCLASSIFIED, "unclassified"), + # `read_image`'s catch-all read failure wraps a bare `except Exception`, so + # one sentinel covers a permissions wall, a corrupt file the agent itself + # wrote, and a decode bug. Nothing here distinguishes them, so it stays out + # of every trip rung (ENG-2248). + "read_failed": (TIER_UNCLASSIFIED, "unclassified"), + # A bare `except Exception` around a PIL round-trip: a missing Pillow is a + # wall, a corrupt BMP is not, and the sentinel cannot tell them apart. + "bmp_convert_failed": (TIER_UNCLASSIFIED, "unclassified"), } # Identifier extraction, per class. Kept narrow on purpose: a wrong identifier @@ -261,7 +302,7 @@ def classify(reason: str, result_text: str = "") -> RootCause: reason = (reason or "").strip()[:_MAX_REASON_CHARS] if not reason: sig = _normalise_error_signature((result_text or "")[:_MAX_REASON_CHARS]) - return RootCause(TIER_UNCLASSIFIED, "unclassified", sig[:80], from_reason=False) + return RootCause(TIER_UNCLASSIFIED, CLS_UNCLASSIFIED, sig[:80], from_reason=False) sentinel = _SENTINEL_REASONS.get(reason) if sentinel: @@ -345,25 +386,51 @@ def classify(reason: str, result_text: str = "") -> RootCause: if exc == "OSError" and not re.search( r"No space|Disk quota|Too many open files|Cannot allocate", reason, re.I ): - return RootCause(TIER_UNCLASSIFIED, "unclassified", + return RootCause(TIER_UNCLASSIFIED, CLS_UNCLASSIFIED, _normalise_error_signature(reason)[:80]) return RootCause(TIER_WALL, wall_cls, _identifier_for(wall_cls, reason)) # Timeouts and kills the runtime words rather than raises. low = reason.lower() if "timed out" in low or "timeout" in low or "inactivity" in low or "liveness" in low: - return RootCause(TIER_TRANSIENT, "timeout", "") + return RootCause(TIER_TRANSIENT, CLS_TIMEOUT, "") if "permission denied" in low: - return RootCause(TIER_WALL, "permission_denied", - _identifier_for("permission_denied", reason)) + return RootCause(TIER_WALL, CLS_PERMISSION_DENIED, + _identifier_for(CLS_PERMISSION_DENIED, reason)) if "connection refused" in low: - return RootCause(TIER_WALL, "connection_refused", - _identifier_for("connection_refused", reason)) + return RootCause(TIER_WALL, CLS_CONNECTION_REFUSED, + _identifier_for(CLS_CONNECTION_REFUSED, reason)) - return RootCause(TIER_UNCLASSIFIED, "unclassified", + return RootCause(TIER_UNCLASSIFIED, CLS_UNCLASSIFIED, _normalise_error_signature(reason)[:80]) +#: Every value `classify` can put in `RootCause.cls`, and every tier. THE +#: authoritative enumeration — a consumer must read these rather than +#: reassembling the tables, which is how `timeout` went missing (ENG-2247). +#: +#: Adding a class means adding it here. That keeps the SET honest; it does not +#: by itself keep a closed-set guard honest, because such a guard only catches +#: an unregistered value if some input actually reaches the branch returning +#: it. Two of ENG-2247's own adversarial inputs missed their branches (an +#: `OSError: ` prefix sent them down `_WALL_TYPES` instead) and the guard +#: still passed. So a new literal needs BOTH an entry here and an input that +#: reaches it. +ALL_CLASSES: frozenset[str] = frozenset( + set(_SELF_INFLICTED) + | set(_TRANSIENT) + | set(_WALL_TYPES.values()) + | {cls for _, cls in _SENTINEL_REASONS.values()} + | set(_STATUS_WALLS.values()) + | {f"http_{code}" for code in _STATUS_TRANSIENT} + | {CLS_UNCLASSIFIED, CLS_TIMEOUT, CLS_PERMISSION_DENIED, CLS_CONNECTION_REFUSED} +) + +ALL_TIERS: frozenset[str] = frozenset( + {TIER_SELF, TIER_TRANSIENT, TIER_WALL, TIER_UNCLASSIFIED} +) + + @dataclass class RootCauseLedger: """Session-scoped tally of classified failures. diff --git a/anton/core/session.py b/anton/core/session.py index 40e8c789..7134a2ba 100644 --- a/anton/core/session.py +++ b/anton/core/session.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -import httpx +import httpx2 as httpx import random from collections.abc import AsyncIterator, Callable from contextlib import aclosing @@ -40,6 +40,7 @@ SCRATCHPAD_TIMEOUT_NUDGE, ) from anton.core.llm.provider import ( + CURATED_PROVIDER_ERRORS, ContextOverflowError, EndpointConfigurationError, LLMResponse, @@ -59,6 +60,7 @@ TransientProviderError, context_window, damaged_tool_call_result, + provider_failure_kind, ) from anton.core.llm.structured import looks_truncated, truncation_verdict, usable_tool_call from anton.core.llm.thalamus import ( @@ -103,7 +105,9 @@ from anton.explainability import ExplainabilityCollector, ExplainabilityStore from anton.utils.datasources import ( + begin_ds_turn_scope, build_datasource_context, + restore_namespaced_env, scrub_credentials, ) from anton.core.settings import CoreSettings @@ -376,22 +380,40 @@ def _null_falls_back_to_default(cls, v: object) -> object: # Output budgets for the verdict call: first attempt, then the retry used when # it comes back truncated (ENG-1081). # -# Models that narrate before acting (`mindshub_air`/`kimi`, `deepseek`, `qwen`) -# spend the budget on prose and never reach the forced tool call. The original -# 256 truncated them on essentially every call — 98.6% of `mindshub_air` verdicts -# in prod returned no tool call, which the fail-safe below turned into a silent +# HISTORY, and read the tense — it is why the numbers are what they are, not a +# claim about today's catalog. In 2026-07 the aliases that narrated before acting +# (`mindshub_air` — then Kimi K2.6 — `kimi`, `deepseek`, `qwen`) spent the budget +# on prose and never reached the forced tool call. The original 256 truncated +# them on essentially every call: 98.6% of `mindshub_air` verdicts in prod +# returned no tool call, which the fail-safe below turned into a silent # "task complete". # +# `mindshub_air` has since been repointed to a GPT model, and re-measured +# 2026-09-03 no MindsHub alias narrates on this call shape at all — 0 narration +# characters on eight of them, with and without `_VERIFIER_NO_PREAMBLE` +# (ENG-1687). Prod agrees: `verifier_failure = 'truncated'` is 0 over 30 days. +# The budget stays anyway. It was sized from a distribution, anton runs on +# arbitrary BYOK and local models nobody has measured, and per the note below +# a model that answers in 43-115 tokens never pays for headroom it doesn't use. +# # 2048 is sized from a measured distribution, not one sample: 16 identical calls -# spanned 245–1654 output tokens (median ~290). That 6.7x per-call spread is also -# why *truncation* never latches — one truncation is a tail sample, not proof -# about the next turn — and why the 4096 retry exists rather than a single bigger -# budget. 1024 was measurably too small. Nothing pays for headroom it doesn't -# use; first-party models answer in 43–115 tokens either way. +# spanned 245–1654 output tokens (median ~290). That 6.7x per-call spread is why +# the 4096 retry exists rather than a single bigger budget. 1024 was measurably +# too small. Nothing pays for headroom it doesn't use; first-party models answer +# in 43–115 tokens either way. +# +# An EXHAUSTED ladder counts toward the latch. `retrying` is false at the last +# budget, so the only truncation that ever reaches the latch check is one that +# blew past 4096 after already blowing past 2048 — never a single sample of a +# model's verbosity. Against the distribution above that is a statement about +# the model. Typed transients never latch, see `_TRANSIENT_VERDICT_ERRORS`. # -# A *hard* failure does latch, which is a different claim: a 400 rejecting the -# forced `tool_choice` is a statement about the model, not about this transcript -# (ENG-1095/ENG-1155). See `_verifier_latched`. +# Accepted cost, worth knowing: unlike a rejected `tool_choice`, this is a +# STOCHASTIC failure. Two unlucky ladders with no successful verdict between +# them latch a model that mostly works, and a turn inside the window ends +# unverified. The threshold of two, a re-probe that a successful verdict +# clears, and the shorter window at `_VERIFIER_LATCH_REPROBE_TURNS_TRUNCATED` +# are what bound that. _VERIFIER_TOKEN_BUDGETS = (2048, 4096) # Verdict-call failures that are transient by nature rather than statements about @@ -420,8 +442,8 @@ def _null_falls_back_to_default(cls, v: object) -> object: # map to TransientProviderError and never land here) or a model id that doesn't # resolve for this key (ModelUnavailableError: 404 model-not-found / 403 # model-access-denied). These latch on the FIRST occurrence and stay silent: -# the turn's work already succeeded, and "the task-completion check failed -# (internal error)" is a lie when the check was merely priced out (ENG-1632 — +# the turn's work already succeeded, and telling the user an internal check +# failed is a lie when the check was merely priced out (ENG-1632 — # of that ticket's 14-day baseline of 296 wallet-402s across 39 users, 208 # were aux-surface calls like this one, across 33 of those users; every aux # one surfaced to the user as an internal error and an apology). @@ -437,12 +459,56 @@ def _null_falls_back_to_default(cls, v: object) -> object: ModelUnavailableError, ) +# Verdict-call outcomes that count toward the latch: a provider that rejects +# the call itself (ENG-1095) and a ladder exhausted by a narrating model. Named +# beside the two sets above so the comments that reason about it point at one +# definition instead of restating it by hand, which is how they drifted before. +_LATCHING_VERDICT_FAILURES = ("hard", "truncated") + +# Verdict calls producing no verdict before the latch engages. Two, because the +# first can be a one-off and ENG-1079 wants its honest diagnosis for that. A +# deterministic denial is the exception: it recurs by construction, so it +# latches on its own branch at the first occurrence. +_VERIFIER_LATCH_THRESHOLD = 2 + # Turns a latched session skips before spending one verdict call to see whether # the cause has gone away (user switched model, gateway fix shipped). Without a # re-probe the latch is permanent for the session and "reset on a successful # verdict" can never fire, since a latched session makes no verdict calls. _VERIFIER_LATCH_REPROBE_TURNS = 10 +# Shorter window when the last thing that failed was a truncation, because a +# re-probe can actually land: output length varies per call (245-1654 tokens +# over 16 identical calls, see `_VERIFIER_TOKEN_BUDGETS`), so a narrating model +# that exhausted the ladder twice may still fit a verdict next time. A provider +# that rejects the forced `tool_choice` rejects every call, so re-probing it +# more often only spends tokens on a certain 400. +# +# The shorter window is the EXPENSIVE one, which is easy to read backwards. A +# truncating re-probe runs the whole ladder before it gives up, so it costs two +# calls, and at 3 turns that is ~0.67 verdict calls per turn against ~0.1 for a +# capability latch at 10. Bought deliberately: recovery is plausible here and +# impossible there, so the spend buys a real chance of verification coming back. +# +# 3 is a judgment, not a measurement, and so was the 10 above it. What would +# settle it is how often a verdict call recovers k turns after a truncation, +# read from turn_completed's verifier_failure per conversation. Nobody has +# read it yet. Do not cite either window as evidence-backed. +_VERIFIER_LATCH_REPROBE_TURNS_TRUNCATED = 3 + + +def _reprobe_turns_for(last_failure: str) -> int: + """Skipped turns before a latched session spends one verdict call again. + + Keyed on the LAST no-verdict class, not the accumulated latch reason: this + is a prediction about what is failing now, and a truncation ten turns ago + says nothing about a model that has been rejecting the call ever since. + """ + if last_failure == "truncated": + return _VERIFIER_LATCH_REPROBE_TURNS_TRUNCATED + return _VERIFIER_LATCH_REPROBE_TURNS + + # Floor for the tokens held back from the spend ceiling (ENG-1286). # # TWO calls land after the last check that passed, not one: the call that @@ -547,6 +613,64 @@ def _safe_error_detail(exc: BaseException) -> str: return name +def _tool_failure_cause(ok: bool | None, reason: str) -> tuple[str, str]: + """`(tier, class)` for one failed tool call, or `("", "")` (ENG-2247). + + Reuses `root_cause.classify` — the SAME vocabulary the turn-level + `root_cause_*` tally is built from — so for a string tool result the row + and its turn cannot disagree about what a failure was. A parallel taxonomy + here would put two spellings of one failure in two fields. + + **Scoped to string results deliberately, and it is not a full agreement + guarantee.** This runs unconditionally at the emit, but + `_record_root_cause` is reached only from the string branch — the + multimodal arms `continue` past it. So a handler returning + `content=[...]` with `ok=False` puts a cause on the tool row that the turn + tally never counts (measured: row `self_inflicted`/`NameError`, tally + `root_cause_failures=0`). Unreachable today — no handler returns + multimodal content at all — but `ToolOutcome.content` permits it, so + whoever migrates the first one must add `_record_root_cause` to both list + arms, alongside the nudge the #308-review NOTE already asks for there. + + **Deliberately does NOT touch `RootCauseLedger`.** That ledger drops every + non-trip-eligible class one line into `add()` (`if not rc.trip_eligible: + return`), and that early return is how "structurally cannot trip" is + implemented for ENG-1531's breaker — widening it to keep `self_inflicted` + names would arm the breaker on the agent's own `NameError`s, the exact + ENG-836 ping-pong the tier exists to exclude. Reading the classifier + directly bypasses the ledger, so nothing a trip rung reads can change. + + `result_text` is deliberately not taken. `classify` consults it only to + build the `identifier` of a reason-less failure; the CLASS in that branch is + the constant `"unclassified"` either way. So the class is identical with or + without it — which is what lets this run at the emit site, where the result + has not been assigned yet, instead of reordering the dispatch loop. + + Only a handler's explicit `ok is False` produces a cause. `ok is None` is an + unmigrated handler (ENG-2248): the event already reports `ok="unknown"`, and + attaching a cause to a call we cannot say failed would invent a verdict. + + **Emit `tier` and `cls` only — never `identifier`, and never `key`.** + `key` is `class:identifier` and is the tempting "more useful" field, but the + identifier is extracted from the reason and carries absolute paths and + internal hostnames verbatim: `permission_denied:/root/.ssh/id_rsa`, + `connection_refused:db.internal.corp:5432`. It is the nearer trap than + `reason`, because it sits on the object this function already destructures. + `test_no_prose_from_the_reason_reaches_the_payload` fails if either is + forwarded — verified by mutation, not by hope. + + Never raises: it runs on the reporting path, where an escape would turn a + handled tool failure into a dead turn. + """ + if ok is not False: + return "", "" + try: + rc = classify_root_cause(reason or "", "") + return rc.tier, rc.cls + except Exception: # pragma: no cover - defensive; classify is total + return "", "" + + def _safe_error_type(exc: BaseException) -> str: """The exception's class name, and nothing else, for analytics. @@ -587,6 +711,33 @@ def _verifier_error_type(exc: BaseException | None) -> str: return name +def _stamp_retry_terminal( + tc: "TurnCost | None", exc: BaseException, reason: str +) -> None: + """Record WHY the retry flow terminated, plus the provider failure behind it. + + Called immediately before each terminal raise so the books carry the split + that `ended_by` and `error_type` cannot express: after ENG-1361 the + request-time and mid-stream terminals raise the SAME + ``ProviderOverloadedError``, and the ``code`` that separates the rate-limit + case is a card contract that never reaches analytics. + + Reads the ORIGINATING exception, not the one about to be raised — the + conversion to ``ProviderOverloadedError`` is exactly where the cause would + otherwise be lost. Never raises: it runs on the failure path, where an + escape would turn a handled failure into a dead turn. + """ + if tc is None: + return + try: + tc.retry_terminal_reason = reason + tc.provider_failure_kind = provider_failure_kind(getattr(exc, "code", None)) + status = getattr(exc, "status_code", None) + tc.provider_http_status = status if isinstance(status, int) else None + except Exception: # pragma: no cover - defensive + pass + + def _is_provider_auth_error(exc: BaseException) -> bool: """Whether ``exc`` is the canonical provider HTTP-401 mapping. @@ -979,6 +1130,9 @@ class ChatSessionConfig: to ChatSession — the session never needs to know where values came from. """ + # Ownership transfers with it: ChatSession.close() closes the client's + # provider transports. A host that shares one client across sessions must + # not close both sessions. llm_client: LLMClient runtime_factory: ScratchpadRuntimeFactory = field(default=local_scratchpad_runtime_factory) cells: list[Cell] | None = None @@ -989,6 +1143,7 @@ class ChatSessionConfig: system_prompt_context: SystemPromptContext = field(default_factory=SystemPromptContext) workspace: Workspace | None = None data_vault: DataVault | None = None + workspace_env_overlay: dict[str, str] | None = None console: Console | None = None initial_history: list[dict] | None = None history_store: HistoryStore | None = None @@ -1124,19 +1279,24 @@ def __init__(self, config: ChatSessionConfig) -> None: # builds a fresh one per HTTP turn, so there this resets every turn. # See `RootCauseLedger` for what that does and does not still measure. self._root_causes = RootCauseLedger() - # Latch for a verifier that fails the same hard way every turn — e.g. - # kimi-K3 rejecting forced `tool_choice` with a 400 (ENG-1095), which - # fails on every verdict call until the gateway fix lands. Without the - # latch, each multi-step turn pays a full-history diagnosis call and - # shows the "checking in" message (ENG-1155). Session-scoped: a hard - # failure twice in a row latches, a successful verdict clears it. - self._verifier_hard_failures = 0 + # Latch for a verifier that produces no verdict the same way every turn + # — kimi-K3 rejecting forced `tool_choice` with a 400 (ENG-1095), or a + # narrating model exhausting the budget ladder. Without it, each + # multi-step turn pays a full-history diagnosis call and shows the + # "checking in" message (ENG-1155). Two such failures with no successful + # verdict between them latch; a successful verdict clears it. Scoped to + # this session: Cowork rebuilds it per message, so a persistent failure + # there still diagnoses per message until a host carries this state. + self._verifier_no_verdict_failures = 0 self._verifier_latched = False self._verifier_latch_skips = 0 - # Why the latch is set — "hard" (capability, ENG-1095) or "denied" - # (billing/model access, ENG-1632) — so the skip log names the real - # cause instead of reporting "0 hard failures" for a denied latch. + # ACCUMULATED evidence: every class that produced no verdict since the + # last success ("hard", "truncated", "denied", or "mixed" once they + # differ). Feeds the skip log and the books. self._verifier_latch_reason = "" + # The LAST such class, which is a different question: it predicts what a + # re-probe would hit, so it picks the window. See `_reprobe_turns_for`. + self._verifier_last_no_verdict = "" self._context_pressure_threshold = s.context_pressure_threshold self._max_consecutive_errors = s.max_consecutive_errors self._resilience_nudge_at = s.resilience_nudge_at @@ -1174,6 +1334,9 @@ def __init__(self, config: ChatSessionConfig) -> None: self._deferred_bundles: dict[str, list["ToolDef"]] = {} self._workspace = config.workspace self._data_vault = config.data_vault + # Kept so an artifact backend can be given the same project .env the + # scratchpad gets; it is never applied to this process. + self._workspace_env_overlay = config.workspace_env_overlay or {} self._console = config.console if self._console is None: # connect_new_datasource's interactive mode needs a terminal to @@ -1256,6 +1419,7 @@ def __init__(self, config: ChatSessionConfig) -> None: workspace_path=config.workspace.base if config.workspace else None, session_id=config.session_id, data_vault=config.data_vault, + workspace_env_overlay=config.workspace_env_overlay, ) self.tool_registry = ToolRegistry() @@ -1399,6 +1563,25 @@ def last_compaction(self) -> dict | None: } + def _note_latch_class(self, failure: str) -> None: + """Record one no-verdict class: accumulated for the books, last for the + window. + + "mixed" once the classes differ, so attribution never depends on which + failure arrived last. A denial is the exception and STAYS the reason: it + is the only class the user can act on, one call failing another way is + no evidence the wallet was topped up, and the re-probe turn books its + own class anyway, so nothing is lost by keeping the actionable label. + """ + self._verifier_last_no_verdict = failure + current = self._verifier_latch_reason + if current == "denied" or failure == "denied": + self._verifier_latch_reason = "denied" + elif not current or current == failure: + self._verifier_latch_reason = failure + elif current != "mixed": + self._verifier_latch_reason = "mixed" + def _record_root_cause( self, tool_ok: bool | None, reason: str, result_text: str ) -> None: @@ -2111,8 +2294,14 @@ def _build_core_tools(self) -> None: async def close(self) -> None: """Clean up scratchpads and other resources.""" - await self._reap_tracked_backends() - await self._scratchpads.close_all() + try: + await self._reap_tracked_backends() + await self._scratchpads.close_all() + finally: + # Provider clients own an HTTP pool. This runs even if the steps + # above raise, or the pool outlives the process. + if self._llm is not None: + await self._llm.aclose() async def emit(self, event) -> None: """Push an out-of-band event to the host, if one is listening. @@ -2752,6 +2941,12 @@ def _emit_turn_cost( # not exotic. Leaving the stale value would let plain Stops appear # in an error-cause breakdown, which is the opposite of the point. tc.error_type = "" + # Same reasoning for ENG-1361's fields, and the same site stamps + # them: a Stop during the summarize call would otherwise book a + # retry terminal that the turn never actually reached. + tc.retry_terminal_reason = "" + tc.provider_failure_kind = "" + tc.provider_http_status = None elif exc is not None: tc.ended_by = "error" # The exception was in scope here and thrown away until ENG-1689, @@ -2769,13 +2964,20 @@ def _emit_turn_cost( except Exception: # pragma: no cover - defensive _root_cause_fields = {} logger.info( - "turn_cost session=%s turn=%d ended_by=%s verification_skipped=%s " + # `attempt` alongside `turn`: `turn_index` is a history position, + # so two attempts of one turn used to produce two indistinguishable + # log lines (ENG-2243). This line is also the ONLY forensics + # surface that survives the collector allowlist (see the note + # below) and, per ENG-2193, the only one a desktop customer has — + # `cowork-server.log`, not PostHog. Without it the new analytics + # property has no fallback at all. + "turn_cost session=%s turn=%d attempt=%s ended_by=%s verification_skipped=%s " "grace_granted=%s grace_tokens=%d " "verifier_failure=%s verifier_error_type=%s tokens_total=%d " "input=%d output=%d cache_read=%d cache_creation=%d " "llm_calls=%d rounds=%d continuations=%d peak_context=%d duration_ms=%d " "by_role=%s %s", - self._session_id, turn_index, tc.ended_by, + self._session_id, turn_index, tc.attempt_id, tc.ended_by, str(tc.verification_skipped).lower(), tc.grace_granted or "-", tc.grace_tokens, tc.verifier_failure, tc.verifier_error_type, tc.total_tokens, @@ -2903,6 +3105,26 @@ def _emit_turn_cost( # `TurnCost.verifier_failure`. verifier_failure=tc.verifier_failure, verifier_error_type=tc.verifier_error_type, + # WHY the retry flow terminated + WHAT failed underneath + # (ENG-1361). `ended_by`/`error_type` cannot express either: + # the request-time and mid-stream terminals now raise the same + # ProviderOverloadedError, and the `code` that splits the + # rate-limit case is a card contract, not analytics. Empty + # strings for turns that did not retry, consistent with every + # other unset string property here. + retry_terminal_reason=tc.retry_terminal_reason, + provider_failure_kind=tc.provider_failure_kind, + # `provider_http_status` is OMITTED rather than sent blank when + # there was no status (a mid-stream failure carried a 200; a + # connection error had no response at all). The transport is + # string-typed, so a placeholder would have to be "" — which + # sorts and groups alongside real statuses and makes the column + # unusable. Absent is the honest encoding of absent. + **( + {"provider_http_status": str(tc.provider_http_status)} + if tc.provider_http_status is not None + else {} + ), tokens_total=str(tc.total_tokens), input_tokens=str(tc.input_tokens), output_tokens=str(tc.output_tokens), @@ -2976,6 +3198,16 @@ def _emit_turn_cost( # forensics. conversation_id=str(self._session_id or ""), turn_index=str(turn_index), + # Unique per turn EXECUTION, where `turn_index` is a history + # position that repeats across retries (ENG-2243). This is the + # key `tool_completed` joins on; `turn_index` stays the field + # the Langfuse trace name and the artifact index are built + # from, so both readings remain available: + # COUNT(DISTINCT turn_attempt_id) -> attempts + # COUNT(DISTINCT conversation_id, turn_index) -> turns + # Absent on a pre-ENG-2243 build: read that as "unknown", + # never as a value. + turn_attempt_id=tc.attempt_id, ) except Exception: # Reporting must never affect the turn that just ran. @@ -2987,9 +3219,29 @@ def _emit_tool_completed( ok: bool | None, duration_ms: float, error_type: str, + reason: str = "", ) -> None: """Per-tool-call analytics event (ENG-1486). + ``reason`` is the handler's own ``ToolOutcome.reason``, taken so the + root-cause class can be derived HERE rather than at each call site + (ENG-2247 review). Deriving it here is what makes it impossible to + forget: there is no cause argument a new dispatch path can omit, and a + caller that passes no ``reason`` at all degrades to ``unclassified`` + — "we do not know why" — instead of to the empty string, which reads + as "this call did not fail". Defaulting the CAUSE was the earlier + shape and it failed in the wrong direction; making it required instead + was worse still, since a missing argument raises ``TypeError`` at the + CALL, outside this method's guard, and kills the turn over telemetry. + + **``reason`` is prose and must never be emitted.** It is a traceback + line or a handler message and carries file paths and user input; that + is the whole reason ``error_type`` was narrowed to a class name. It is + consumed by ``_tool_failure_cause`` two lines down and never touched + again — do not add it, ``rc.identifier`` or ``rc.key`` to the payload. + ``test_no_prose_from_the_reason_reaches_the_payload`` and the + exact-keys assertion both fail if you do. + The verdict the dispatch loop already computes for the UI's ``tool_done`` marker, sent where it can be aggregated: without this, "which tools fail, how often, how slowly" is unanswerable — the @@ -3003,17 +3255,27 @@ def _emit_tool_completed( is reported as ``"unknown"`` rather than coerced either way. Payload is deliberately name + verdict + duration + exception CLASS - + surface (a closed enum, ENG-1945) plus the two join keys, and - nothing else. Arguments, result content and ``str(exc)`` routinely + + surface (a closed enum, ENG-1945) + the root-cause tier and class + (both closed vocabularies, ENG-2247) plus the THREE join keys + (``conversation_id``, ``turn_index``, ``turn_attempt_id``), and + nothing else — ten keys, and the exact-keys assertion in + ``test_tool_completed.py`` is what keeps that number honest. This + paragraph is the privacy-audit enumeration, so keep it in step with + the payload: arguments, result content and ``str(exc)`` routinely carry file paths, user data and credentials-adjacent strings — none of them may ever appear here. - ``conversation_id`` / ``turn_index`` mirror ``turn_completed``'s - values exactly (same names, same derivation), so a tool failure spotted - in PostHog joins to its parent turn row there and, via - ``conversation_id`` → Langfuse ``sessionId``, to the gateway trace of - the turn it happened in. Both already ride ``turn_completed`` through - this same sink — no new privacy surface. + ``conversation_id`` / ``turn_index`` / ``turn_attempt_id`` mirror + ``turn_completed``'s values exactly (same names, same derivation), so a + tool failure spotted in PostHog joins to its parent turn row there and, + via ``conversation_id`` → Langfuse ``sessionId``, to the gateway trace + of the turn it happened in. All three already ride ``turn_completed`` + through this same sink — no new privacy surface. + + Prefer ``turn_attempt_id`` for that join: ``turn_index`` is a history + position and repeats across every retry of the same turn, so + ``(conversation_id, turn_index)`` matched more than one turn row for + 18.5% of these rows before ENG-2243. """ try: # Same settings resolution as `_emit_turn_cost` above: the @@ -3026,6 +3288,10 @@ def _emit_tool_completed( settings = AntonSettings() from anton.analytics import send_event + # Derived here, not handed in — see the docstring. `reason` is + # consumed and dropped; only the two closed-vocabulary tokens go on. + _rc_tier, _rc_class = _tool_failure_cause(ok, reason) + # send_event takes STRING values only — every extra is a wire # parameter (tests/test_ask_user.py:496 exists because a first # draft assumed a (name, props) shape). @@ -3037,6 +3303,12 @@ def _emit_tool_completed( turn_index = ( getattr(_tc, "turn_index", 0) or (self._turn_count + 1) ) + # Read from the SAME books as `turn_index` above, so a tool row and + # its parent turn row can never disagree about which attempt they + # belong to (ENG-2243). Empty outside a turn — the tool ran with no + # books open, which is not an attempt and must not be given an id + # that looks like one. + turn_attempt_id = str(getattr(_tc, "attempt_id", "") or "") send_event( settings, "tool_completed", @@ -3054,6 +3326,20 @@ def _emit_tool_completed( surface=str(getattr(self, "_surface", None) or ""), conversation_id=str(self._session_id or ""), turn_index=str(turn_index), + # WHY the call failed, in `root_cause.py`'s vocabulary + # (ENG-2247). `error_type` above only fills in when the failure + # was a RAISE; `scratchpad` — 79% of tool volume, 9.35% of it + # failing — returns a verdict instead, so 83% of failures + # reached analytics with no cause at all. Both empty on success + # and on an unmigrated handler; see `_tool_failure_cause`. + root_cause_tier=_rc_tier, + root_cause_class=_rc_class, + # Makes the tool -> turn join exact (ENG-2243). Before this, + # `(conversation_id, turn_index)` matched every retry of the + # same turn, so 18.5% of these rows joined to more than one + # turn row and "which tools ran in the attempt that hit the + # spend ceiling" had no answer. + turn_attempt_id=turn_attempt_id, ) except Exception: # Analytics must never affect the tool call that just ran. @@ -3249,6 +3535,7 @@ async def plan_stream_with_recovery( tools: list[dict] | None = None, max_tokens: int | None = None, messages_factory: Callable[[], list[dict]] | None = None, + allow_native_web_tools: bool = True, ) -> AsyncIterator[StreamEvent]: """Streaming analogue of plan_with_recovery. @@ -3256,6 +3543,16 @@ async def plan_stream_with_recovery( ContextOverflowError, yields StreamContextCompacted, shrinks history (summarize+compact, then hard-truncate on a repeat overflow), and restarts the stream. A fourth overflow propagates. + + ``allow_native_web_tools=False`` suppresses the session's native web + tools for this call. Default True, so every agent-loop caller is + unchanged: there the tools are the point. It exists for the + retry-exhausted wrap-up (ENG-1361 review of #433), which asks the model + to STOP and explain a failure — handing it a research tool contradicts + the instruction, and the prompt embeds the raw error, so a provider-side + search could carry file paths or user content from it to a search + backend. That call needs this method for the COMPACTION, not for the + capabilities that ride along with it. """ factory = messages_factory if messages_factory is not None else (lambda: self._history) # Same defensive pre-flight as plan_with_recovery — see the @@ -3270,7 +3567,7 @@ def factory_validated(): kwargs["tools"] = tools if max_tokens is not None: kwargs["max_tokens"] = max_tokens - if self._native_web_tools: + if allow_native_web_tools and self._native_web_tools: kwargs["native_web_tools"] = self._native_web_tools try: @@ -3591,7 +3888,25 @@ def _inject_recalled_skills(self, labels: list[str]) -> None: self._append_history({"role": "assistant", "content": tool_uses}) self._append_history({"role": "user", "content": results}) + def _open_ds_turn_scope(self) -> None: + """Open this turn's DS_* scope and rebuild it from the session's vault. + + Rebuilt here rather than trusted from the host: the pod builds its + session in a `run_in_executor` worker, whose ContextVar writes never + reach this task, which would leave the turn scrubbing against nothing. + """ + begin_ds_turn_scope() + if self._data_vault is None: + return + try: + restore_namespaced_env(self._data_vault) + except Exception: + logger.warning( + "Could not rebuild this turn's DS_* scrub state", exc_info=True + ) + async def turn(self, user_input: str | list[dict]) -> str: + self._open_ds_turn_scope() user_input = _scrub_user_input(user_input) # Stamp the inbound user turn here, not in _append_history: tool_result # and synthetic user-role messages also flow through append and must @@ -3800,6 +4115,7 @@ async def turn(self, user_input: str | list[dict]) -> str: ok=outcome.ok, duration_ms=(_time.monotonic() - _tool_t0) * 1000.0, error_type=_tool_error_type, + reason=outcome.reason, ) result = outcome.content @@ -3983,6 +4299,9 @@ async def turn_stream( is what makes questions unavailable on the non-streaming `turn()` path: nothing there would render them. """ + # Before any tool task is spawned, so a connect made mid-turn registers + # into a container this turn still holds. + self._open_ds_turn_scope() self.emitter = TurnEmitter() self.question_count = 0 self.answer_wait_s = 0.0 @@ -4220,6 +4539,9 @@ async def _turn_stream_inner( # decide it for them by stalling the turn. _hint = getattr(_agent_exc, "retry_after", None) if _rate_limited and _hint is not None and _hint > max_delay: + _stamp_retry_terminal( + self._turn_cost, _agent_exc, "rate_limit_wait_too_long" + ) raise ProviderOverloadedError( "Too many requests too quickly — the limit clears in about " f"{int(_hint)}s. This isn't a credits problem.", @@ -4278,6 +4600,9 @@ async def _turn_stream_inner( # one. Never say "incident": nothing is broken, and # never imply credits — buying more cannot raise a # per-minute ceiling (ENG-1537). + _stamp_retry_terminal( + self._turn_cost, _agent_exc, "rate_limit_wait_limit" + ) raise ProviderOverloadedError( "Too many requests too quickly — the rate limit didn't " "clear in time. Waiting a moment and continuing should work; " @@ -4287,6 +4612,9 @@ async def _turn_stream_inner( code="rate_limited", retry_after=getattr(_agent_exc, "retry_after", None), ) from _agent_exc + _stamp_retry_terminal( + self._turn_cost, _agent_exc, "provider_recovery_timeout" + ) raise ProviderOverloadedError( f"{_agent_exc.provider or 'The model provider'} is experiencing an " "incident and didn't recover in time.", @@ -4332,12 +4660,99 @@ async def _turn_stream_inner( # again with the error context now in history continue else: + # A transient that outlasted the COUNT budget is the + # same product event as one that outlasted the TIME + # budget on the backoff path above: the provider is + # unreachable and the user needs the provider_overloaded + # card — with Retry, and the MindsHub failover nudge for + # BYOK — not prose (ENG-1361). + # + # Before this, the count path had NO exit that produced + # a card: it asked the model to explain the outage, a + # call that needs the very provider that is failing, and + # when that call failed too the turn ended as "An + # unexpected error occurred: . Please + # try again or rephrase your request." Rephrasing cannot + # reach an unreachable provider, and users followed the + # advice — the incident this ticket came from shows the + # user retyping the same request, then leaving. + # + # Raised BEFORE the SYSTEM message is appended below: + # appending first would leave "The task has failed N + # times" dangling in a history the next turn replays. + # + # KNOWINGLY DEFERRED: this is scoped on the exception + # TYPE, so it also sweeps in the `bad_response` codes + # (`empty_response`, `truncated_stream`) — which anton + # classifies as "a weak incident signal and a STRONG + # broken/misconfigured-endpoint signal", and which + # therefore inherit a card whose primary action is + # Retry (and, in the CLI, a prompt defaulting to + # `retry` rather than `setup`). For a wrong base URL + # that is the wrong steer. It is not a regression — the + # prose this replaces also said "try again in a moment" + # — and the signal is genuinely ambiguous by the + # classifier's own wording, so it is left alone rather + # than guessed at. `provider_failure_kind=bad_response` + # (added by this change) is what will size the + # population; routing it is ENG-2264. + if isinstance(_agent_exc, TransientProviderError): + _stamp_retry_terminal( + self._turn_cost, _agent_exc, "request_attempt_limit" + ) + # Name the model that actually failed (planning OR + # coding) so the card's provider lookup — and so the + # BYOK-vs-managed nudge it picks — is right. + _model = (getattr(_agent_exc, "model", "") or "") or getattr( + self._llm, "planning_model", "" + ) or "" + # NO rate-limit special case here, deliberately. + # A 429 reaching the COUNT path is by definition one + # `classify_transient` could NOT confirm was a + # velocity limit (`session_backoff=velocity_confirmed`), + # and its docstring names that exact population: a + # daily quota in a dialect the string-exact billing + # guards miss (Gemini's RESOURCE_EXHAUSTED) "would + # otherwise spend the whole budget waiting out a + # daily quota that resets at midnight — then be told + # it is not a credits problem." Promising "waiting + # should work" and denying a credits problem is + # precisely that mis-report, on precisely that + # population. It also buys nothing: an unconfirmed + # 429 carries `retry_after=None` (same line), so the + # rate_limited card has no interval to time-gate its + # Retry with. The generic branch below is honest — + # anton's own typed message, no claim either way. + # KEEP anton's own classification in the copy. The + # typed message already says what happened ("The + # model provider returned 500.") and ENG-673 put it + # there deliberately; replacing it with a generic + # "could not be reached" would throw away the one + # detail that tells a user — or a support thread — + # which failure this was. Only the attempt count is + # new information. + _detail = str(_agent_exc).rstrip() + if _detail and _detail[-1] not in ".!?": + _detail += "." + raise ProviderOverloadedError( + f"{_detail} It did not recover after {_retry_count} attempts.", + provider=getattr(_agent_exc, "provider", "") or "", + model=_model, + ) from _agent_exc # Exhausted retries — stop and summarize for the user. # Mark the terminal: the apology below is yielded as # ordinary text and nothing is in flight when the # finally runs, so without this the turn reported # "completed" — undercounting the most common failure # mode in any error-rate query (#309 review). + # + # Still reached by NON-transient failures (a 400, a tool + # crash), which keep the summarize-and-explain behaviour: + # for those the model genuinely can say something useful, + # and there is no card to route them to. + _stamp_retry_terminal( + self._turn_cost, _agent_exc, "request_attempt_limit" + ) if self._turn_cost is not None: self._turn_cost.ended_by = "retry_exhausted" # Same exception the SYSTEM message below shows the @@ -4352,47 +4767,88 @@ async def _turn_stream_inner( "Stop retrying. Please:\n" "1. Summarize what you accomplished so far.\n" "2. Explain what went wrong in plain language.\n" - "3. Suggest next steps — what the user can try (e.g. rephrase, " - "simplify the request, or ask you to continue from where you left off).\n" + "3. Suggest next steps — but only ones that follow from the " + "error above. Do NOT suggest rephrasing or simplifying the " + "request unless the error was actually about what was asked; " + "for an infrastructure or provider failure, say plainly that " + "retrying later is the remedy.\n" "Be concise and helpful." ), } ) try: self._validate_history_for_provider(self._history) - async for event in self._llm.plan_stream( + # ...with_recovery, not the raw call: a history too + # long to SUMMARIZE is the one overflow where + # shrinking it is exactly the remedy, and this is + # the only plan_stream site that lacked it. Without + # it a ContextOverflowError here propagates to a + # card-less generic "An unexpected error occurred." + # — strictly less than the prose it replaced, and on + # the one failure whose fix anton can perform + # itself. A fourth consecutive overflow still + # propagates (ENG-1361 review). + async for event in self.plan_stream_with_recovery( system=await self._build_system_prompt(user_msg_str), - messages=self._history, + # This call wants the compaction, NOT the + # session's web tools: the prompt above says + # "Stop retrying" and asks for an explanation, + # and it embeds the raw error text. Leaving them + # on would both contradict the instruction and + # let a provider-side search carry paths or user + # content out of that error (#433 review). + allow_native_web_tools=False, ): if isinstance(event, StreamTextDelta): assistant_text_parts.append(event.text) yield event except Exception as e: - if isinstance(e, (TokenLimitExceeded, ModelUnavailableError, EndpointConfigurationError)): + if isinstance(e, CURATED_PROVIDER_ERRORS): # Curated provider failures must FAIL the turn, not # get wrapped into assistant prose: the server maps - # token_limit/model_unavailable to actionable cards, - # which can only fire when the exception propagates. - # Wrapping them as text is how "Server returned 403" - # ended up mid-chat with "please rephrase your - # request" advice. EndpointConfigurationError added - # here to match the immediate re-raise site above — - # this wrap-up call had been the one place it still - # fell through (review feedback on ENG-1310). NOTE: - # cowork-server has no dedicated card for - # EndpointConfigurationError yet (grepped — zero - # hits, friendly_turn_error falls through to the - # generic message for it); re-raising it here still - # stops the misleading "adjust your approach" prose, - # it just doesn't get a *better* card until that - # mapping exists server-side. + # them to actionable cards, which can only fire when + # the exception propagates. Wrapping them as text is + # how "Server returned 403" ended up mid-chat with + # "please rephrase your request" advice. + # + # ENG-1361 moved the membership list from HERE to + # `provider.py`, beside the classes themselves, so + # a new type is triaged where it is born. + # + # Be precise about what that does and does NOT buy + # (review of #433): this is STILL an allowlist at + # runtime — an unlisted type still becomes prose. + # The default-safe property comes only from + # `test_every_exception_defined_in_a_provider_module_is_triaged`, + # which is why that test's module list must cover + # every module that defines one of these. + # + # And propagating is not automatically better. + # Four members currently have NO card on either + # transport (ContextOverflowError, + # StructuredOutputError, TransientProviderError, + # EndpointConfigurationError): cowork-server + # replaces the message with a flat "An unexpected + # error occurred." and the client renders a + # BUTTONLESS alert, so for those the turn trades + # anton's own diagnosis for less text. It is still + # the right trade — the prose asserted a remedy + # that could not work — but it is a trade, not a + # free win, and the cards are the follow-up. raise if _is_provider_auth_error(e): # Preserve a refusal after failed confirmation, # or the first refusal after stream output, for # the host's auth-error mapping. raise - fallback = f"An unexpected error occurred: {e}. Please try again or rephrase your request." + # No "rephrase your request": everything reaching + # this line is an UNEXPECTED failure, and we have no + # basis to claim the user's wording caused it. The + # curated failures — where we DO know the cause, and + # where rephrasing provably cannot help — are re- + # raised above (ENG-1361). Say what happened and stop + # inventing a remedy. + fallback = f"An unexpected error occurred: {e}" assistant_text_parts.append(fallback) yield StreamTextDelta(text=fallback) break @@ -5179,6 +5635,7 @@ async def _stream_and_handle_tools( 0.0, ) * 1000.0, error_type=_tool_error_type, + reason=tool_reason, ) if isinstance(result_text, list): @@ -5392,9 +5849,9 @@ async def _stream_and_handle_tools( # Consolidation still runs after diagnosis break - # A verifier that already failed hard twice in a row will keep - # failing for the rest of the session (ENG-1095's forced-tool_choice - # 400 is per-model, not per-call). Skip the verdict rather than pay a + # A verifier that already produced no verdict twice will keep + # failing for this model (ENG-1095's forced-tool_choice 400 is + # per-model, not per-call). Skip the verdict rather than pay a # full-history diagnosis every turn and show "checking in" each time # (ENG-1155). The turn ends on the model's own answer, which is # already in history — unverified, but that beats a per-turn @@ -5403,32 +5860,34 @@ async def _stream_and_handle_tools( # user may switch off the broken model mid-session, or the gateway # fix may land. Without this, "reset on a successful verdict" is # unreachable — a latched session skips every verdict call, so there - # is never another success to reset on. A hard-failing re-probe stays - # latched and does NOT re-diagnose (the latched branch below breaks - # before the diagnosis), so the cost is one verdict call per - # _VERIFIER_LATCH_REPROBE_TURNS turns. - # A re-probe that TRUNCATES (or hits a typed TransientProviderError) - # instead is intended to fall through to the honest diagnosis below, - # and it leaves the latch set: neither counts toward or against the - # latch (truncation is a tail sample, see _VERIFIER_TOKEN_BUDGETS; - # typed transients never latch by definition), so only a successful - # verdict clears it. A latched session re-probing into a - # persistently-verbose model therefore diagnoses once per re-probe - # cycle rather than never — matching "every truncated turn keeps its - # honest diagnosis". + # is never another success to reset on. A re-probe that produces no + # verdict stays latched and does NOT re-diagnose (the latched branch + # below breaks before the diagnosis), so the cost is one FAILED + # verdict attempt per window — which for a truncation is the whole + # ladder, two calls, not one. See `_reprobe_turns_for` for why the + # window depends on what failed last. A re-probe hitting a typed + # TransientProviderError still falls through to the honest + # diagnosis and leaves the latch set — those never latch by + # definition, so only a successful verdict clears it. if self._verifier_latched: self._verifier_latch_skips += 1 - if self._verifier_latch_skips < _VERIFIER_LATCH_REPROBE_TURNS: + reprobe_turns = _reprobe_turns_for(self._verifier_last_no_verdict) + if self._verifier_latch_skips < reprobe_turns: _verifier_log.info( "completion-verifier skipped — latched (%s) " "(skip %d/%d before re-probe); " "continuation=%d/%d tool_rounds=%d", + # States what happened, never why it was enough: a + # denial latches on call one and a failed re-probe can + # leave the count below the threshold, so any sentence + # naming the threshold here would sometimes be false. "deterministic denial: billing or model access" if self._verifier_latch_reason == "denied" - else f"{self._verifier_hard_failures} hard failures " - "with no successful verdict between them", + else f"{self._verifier_latch_reason}, " + f"{self._verifier_no_verdict_failures} verdict call(s) with " + "no successful verdict since", self._verifier_latch_skips, - _VERIFIER_LATCH_REPROBE_TURNS, continuation, + reprobe_turns, continuation, self._max_continuations, tool_round, ) # Stamp the books: this turn books ended_by="completed" @@ -5441,9 +5900,11 @@ async def _stream_and_handle_tools( self._turn_cost.verification_skipped = True # ...and WHY it was skipped (ENG-1858): no call was # made this turn, so there is no exception type — the - # latch reason is the whole story. + # latch reason is the whole story. No fallback: the + # reason is always set before the latch, and guessing + # "hard" here would file a capability claim we never saw. self._turn_cost.verifier_failure = ( - f"latched_{self._verifier_latch_reason or 'hard'}" + f"latched_{self._verifier_latch_reason}" ) break _verifier_log.info( @@ -5459,9 +5920,9 @@ async def _stream_and_handle_tools( self._history, user_message ) verdict = None - # Truncation vs hard failure decides whether this counts toward the - # latch: a blown budget is a tail sample of one model's verbosity, - # an identical hard error twice is a capability problem (ENG-1155). + # Which class of no-verdict failure this was. Everything except a + # typed transient counts toward the latch; the class is kept so the + # books can name it (ENG-1155/ENG-1858). verdict_failure: str | None = None # The last attempt's exception, kept only long enough to stamp # its TYPE on the turn books below (ENG-1858). `except ... as exc` @@ -5486,11 +5947,15 @@ async def _stream_and_handle_tools( ) verdict_failure = "truncated" if exc.truncated else "hard" verdict_exc = exc - _verifier_log.info( + # WARNING only when this attempt ends the loop: a truncation + # the larger budget recovers from is not a failure to report. + _verifier_log.log( + _logging.INFO if retrying else _logging.WARNING, "completion-verifier verdict=%s budget=%d output_tokens=%d " - "stop_reason=%s retrying=%s", + "stop_reason=%s error=%s retrying=%s", truncation_verdict(exc), - budget, exc.output_tokens, exc.stop_reason, retrying, + budget, exc.output_tokens, exc.stop_reason, + _safe_error_detail(exc), retrying, ) if not retrying: break @@ -5518,9 +5983,9 @@ async def _stream_and_handle_tools( # user buys nothing: handled below by latching silently. verdict_failure = "denied" verdict_exc = exc - _verifier_log.info( - "completion-verifier verdict=DENIED budget=%d error=%s", - budget, _safe_error_detail(exc), + _verifier_log.warning( + "completion-verifier verdict=DENIED budget=%d model=%s error=%s", + budget, self._llm.coding_model, _safe_error_detail(exc), ) break except _TRANSIENT_VERDICT_ERRORS as exc: @@ -5535,7 +6000,7 @@ async def _stream_and_handle_tools( # latch. See `_TRANSIENT_VERDICT_ERRORS` for what qualifies. verdict_failure = "transient" verdict_exc = exc - _verifier_log.info( + _verifier_log.warning( "completion-verifier verdict=TRANSIENT budget=%d error=%s", budget, _safe_error_detail(exc), ) @@ -5547,7 +6012,7 @@ async def _stream_and_handle_tools( # content (ENG-1081). verdict_failure = "hard" verdict_exc = exc - _verifier_log.info( + _verifier_log.warning( "completion-verifier verdict=ERROR budget=%d error=%s", budget, _safe_error_detail(exc), ) @@ -5557,9 +6022,10 @@ async def _stream_and_handle_tools( # A working verdict clears the latch: whatever was failing # (transient provider error, a model the user has since changed) # is no longer failing. - self._verifier_hard_failures = 0 + self._verifier_no_verdict_failures = 0 self._verifier_latched = False self._verifier_latch_reason = "" + self._verifier_last_no_verdict = "" status = verdict.status reason = verdict.reason.strip() else: @@ -5597,59 +6063,70 @@ async def _stream_and_handle_tools( # resolution (cowork-server, same ticket); this branch is # the guarantee the user is never told the turn failed. self._verifier_latched = True - self._verifier_latch_reason = "denied" + # Through the same accumulator as every other class, but + # WITHOUT touching the counter: this branch latches on call + # one, so the threshold never applies to it. + self._note_latch_class("denied") + # INFO, unlike the other latch lines: this one latches on + # call one, so at WARNING it doubles verdict=DENIED above. _verifier_log.info( "completion-verifier latched after a deterministic " - "denial (billing or model access) — skipping further " - "verification this session; turn ends on the " + "denial (billing or model access) — skipping " + "verification until the next re-probe; turn ends on the " "already-streamed reply" ) # Unverified turn — see the latched-skip stamp above. if self._turn_cost is not None: self._turn_cost.verification_skipped = True break - if verdict_failure == "hard": - self._verifier_hard_failures += 1 + if verdict_failure in _LATCHING_VERDICT_FAILURES: + self._verifier_no_verdict_failures += 1 + # Before the latched check, so a failed re-probe joins the + # evidence rather than leaving the books naming only + # whichever class happened to latch first. + self._note_latch_class(verdict_failure) if self._verifier_latched: # A failed re-probe: the cause is still there. Stay # latched with its own log line — re-announcing "latched # after N failures" with an ever-growing N would read as # a new event each cycle. No re-diagnosis (one per # session, ENG-1155). - _verifier_log.info( + _verifier_log.warning( "completion-verifier re-probe failed — staying latched" ) # Unverified turn — see the latched-skip stamp above. if self._turn_cost is not None: self._turn_cost.verification_skipped = True break - if self._verifier_hard_failures >= 2: - # Second hard failure in a row: the first could have been - # transient, this one establishes the pattern. Latch and - # skip the diagnosis on this turn too — a second + if self._verifier_no_verdict_failures >= _VERIFIER_LATCH_THRESHOLD: + # Threshold reached: the cause is established, so latch + # and skip the diagnosis on this turn too — a second # "checking in" message plus a second full-history call # buys nothing once the cause is known to recur. One # diagnosis per session (ENG-1155). # - # "Hard" = neither truncation nor anything in - # `_TRANSIENT_VERDICT_ERRORS` (see that except-clause - # above). Connection drops and timeouts are transient and - # never latch; what remains is the shape ENG-1095 has — - # a provider that rejects the call itself. + # Counted: a provider that rejects the call itself + # (ENG-1095's 400 on forced `tool_choice`) and a ladder + # exhausted by a narrating model. Not counted: anything + # in `_TRANSIENT_VERDICT_ERRORS`; a deterministic denial + # latched on its own branch above. + # + # Not strictly "consecutive": the counter is reset only + # by a *successful* verdict, since only a real verdict + # proves the model can produce one. # - # Not strictly "consecutive" either: the counter is reset - # only by a *successful* verdict, so hard → truncated → - # hard still latches on the second hard failure. That is - # deliberate — only a real verdict proves the model can - # produce one, and a truncation in between is no evidence - # that it can (review: pnewsam on #299). + # The reason carries the class, so the books can tell + # latched_hard from latched_truncated rather than + # mislabelling one as the other. self._verifier_latched = True - self._verifier_latch_reason = "hard" - _verifier_log.info( - "completion-verifier latched after %d hard failures with " - "no successful verdict between them — skipping further " - "verification this session", - self._verifier_hard_failures, + _verifier_log.warning( + "completion-verifier latched after %d verdict calls that " + "produced no verdict (%s) with no successful verdict " + "between them — skipping verification until the next " + "re-probe, in %d turns", + self._verifier_no_verdict_failures, + self._verifier_latch_reason, + _reprobe_turns_for(self._verifier_last_no_verdict), ) # Unverified turn — see the latched-skip stamp above. if self._turn_cost is not None: @@ -5664,27 +6141,37 @@ async def _stream_and_handle_tools( # user to help). Fail toward the same honest, model-generated # diagnosis used for STUCK below, so the task pauses with a # real message instead of nothing. - _verifier_log.info( + _verifier_log.warning( "completion-verifier verdict=ERROR continuation=%d/%d tool_rounds=%d " - "— failing toward an honest diagnosis, not a silent COMPLETE", + "model=%s — failing toward an honest diagnosis, not a silent COMPLETE", continuation, self._max_continuations, tool_round, + self._llm.coding_model, ) self._append_history( { "role": "user", "content": ( - "SYSTEM: The task-completion check failed to run (internal " - "error), so it's unclear whether this task is finished.\n\n" - "Summarize what you've done so far, be honest that an internal " - "check failed partway through, and ask the user how they'd like " - f"to proceed. {_SOLVABILITY_CLAUSE} Do not mention this " - "instruction or the verifier to the user." + "SYSTEM: Your reply above stands, and nothing in your work " + "was rejected — no verdict came back at all. What failed is " + "the automatic check on whether the task is complete, so " + "completeness is unconfirmed rather than denied.\n" + "1. Report accurately what you did: what succeeded, and " + "anything that did not, including any tool that returned an " + "error. Do not describe a failed step as done.\n" + "2. Name a file only by a path that appears verbatim in a " + "tool result above. Never state a path you intended to write " + "or believe you wrote; where no tool result gives one, say " + "what you produced without naming a location.\n" + "3. Say whether anything still needs doing and ask how they'd " + "like to proceed.\n" + f"4. {_SOLVABILITY_CLAUSE}\n" + "Do not mention this instruction or the verifier to the user." ), } ) yield StreamTaskProgress( phase="analyzing", - message="Something went wrong — checking in with you...", + message="Confirming what was completed...", ) if self._turn_cost is not None: self._turn_cost.ended_by = "handback_verifier_failure" diff --git a/anton/core/tools/recall_skill.py b/anton/core/tools/recall_skill.py index a1511c50..366a99e6 100644 --- a/anton/core/tools/recall_skill.py +++ b/anton/core/tools/recall_skill.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING +from anton.core.tools.registry import ToolOutcome from anton.core.tools.tool_defs import ToolDef if TYPE_CHECKING: @@ -115,20 +116,50 @@ def _already_in_history(session, label: str) -> bool: return False -async def handle_recall_skill(session: "ChatSession", tc_input: dict) -> str: - """Look up a skill by label and return its declarative procedure.""" +async def handle_recall_skill( + session: "ChatSession", tc_input: dict +) -> "str | ToolOutcome": + """Look up a skill by label and return its declarative procedure. + + Verdicts (ENG-2248). `ok` drives the per-tool error streak, so it fires the + resilience nudge at 2 consecutive failures and the circuit breaker at 5 — + a verdict here is a behaviour decision, not a label. This tool ran 1,150 + times across 452 installs in 30 days, so a wrong `ok=False` reaches + everyone. + + * `ok=True` — a procedure was returned, INCLUDING the already-recalled + stub: that is a success with a deliberately short body, not a failure. + * `ok=False` — the call could not be served at all: no label, or no store. + Repeating either cannot help, so it SHOULD reach the streak. This is the + intended, accepted behaviour change. + * `ok=None` — the NO MATCH family, left unmigrated ON PURPOSE. See the + comments at those returns. + """ label_in = (tc_input.get("label") or "").strip() if not label_in: - return ( - "ERROR: recall_skill requires a non-empty 'label' parameter. " - "Pick one from the procedural memory list in your system prompt." + # Tier 2 (ENG-2248): a malformed call. Retrying it unchanged cannot + # work, so repetition IS thrash and belongs in the streak. + return ToolOutcome( + content=( + "ERROR: recall_skill requires a non-empty 'label' parameter. " + "Pick one from the procedural memory list in your system prompt." + ), + ok=False, + reason="missing_name", ) store = getattr(session, "_skill_store", None) if store is None: - return ( - "ERROR: no skill store is wired into this session. " - "Procedural memory is unavailable right now." + # Tier 2: the host wired no store, so no label can ever work. + # `store_unavailable` is an existing `_SENTINEL_REASONS` key mapping to + # external_wall/service_unavailable — reused, not invented. + return ToolOutcome( + content=( + "ERROR: no skill store is wired into this session. " + "Procedural memory is unavailable right now." + ), + ok=False, + reason="store_unavailable", ) skill = store.load(label_in) @@ -137,6 +168,13 @@ async def handle_recall_skill(session: "ChatSession", tc_input: dict) -> str: closest = store.closest_match(label_in) if closest is None: available = [s["label"] for s in store.list_summaries()] + # Tier 3 (ENG-2248): deliberately ok=None, NOT a failure. The tool + # worked — it looked, found nothing, and told the model to proceed. + # This is the most common non-success on the highest-volume tool, so + # ok=False here would push normal exploration into the error streak + # and trip the breaker on correct behaviour. If it should ever + # count, that needs its own ticket and its own before/after on the + # nudge rate. if not available: return ( f"NO MATCH: no skill named '{label_in}', and the procedural " @@ -148,7 +186,11 @@ async def handle_recall_skill(session: "ChatSession", tc_input: dict) -> str: ) skill = store.load(closest) if skill is None: - # Race or filesystem flake — be defensive + # Race or filesystem flake — be defensive. + # Tier 3: left ok=None with the rest of the NO MATCH family. + # Arguably a real failure (a load that should have worked), but it + # is indistinguishable from a plain miss without a store-level + # signal, and guessing wrong costs a breaker trip. return ( f"NO MATCH: '{label_in}' was not found and the closest " f"candidate '{closest}' could not be loaded." @@ -171,18 +213,29 @@ async def handle_recall_skill(session: "ChatSession", tc_input: dict) -> str: # procedure header — otherwise a stub surviving compaction would # satisfy _already_in_history forever and the full contract would # never be re-sent. - return ( - f"Skill '{skill.label}' was already recalled in this conversation " - "— its full procedure is in your context above, under the " - f"'# Skill: {skill.name}' heading, and still applies. Not " - "re-sending the body." + # Tier 1 (ENG-2248): a SUCCESS with a deliberately short body. The + # procedure is already in context and still applies, so the tool did + # its job. Behaviourally identical to today — a bare-string return + # already resets the streak, since the legacy matcher finds none of its + # five markers in this text. + return ToolOutcome( + content=( + f"Skill '{skill.label}' was already recalled in this conversation " + "— its full procedure is in your context above, under the " + f"'# Skill: {skill.name}' heading, and still applies. Not " + "re-sending the body." + ), + ok=True, ) # Increment the recommended counter for the *resolved* label, not the # input. If the LLM typo'd 'csv-sumary', we credit 'csv-summary'. store.increment_recommended(skill.label, stage=1) - return _format_skill_response(skill, warning=warning) + # Tier 1: the procedure was returned. Also covers the closest-match path, + # where `warning` explains the substitution — a substitution is still a + # served request. + return ToolOutcome(content=_format_skill_response(skill, warning=warning), ok=True) RECALL_SKILL_TOOL = ToolDef( diff --git a/anton/core/tools/skill_draft.py b/anton/core/tools/skill_draft.py index 943075bc..b33b455f 100644 --- a/anton/core/tools/skill_draft.py +++ b/anton/core/tools/skill_draft.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING from anton.core.tools.skill_format import SKILL_FILE, normalize_name +from anton.core.tools.registry import ToolOutcome from anton.core.tools.tool_defs import ToolDef if TYPE_CHECKING: @@ -88,36 +89,55 @@ def _seed_from_store(folder: Path, slug: str, store) -> None: logger.warning("skill draft %r: could not seed from the store", slug, exc_info=True) -async def handle_create_skill_draft(session: "ChatSession", tc_input: dict) -> str: +async def handle_create_skill_draft( + session: "ChatSession", tc_input: dict +) -> "str | ToolOutcome": """Claim `/`; return `{slug, path, skill_file}`.""" root = getattr(session, "_skill_drafts_root", None) if root is None: - return json.dumps({"error": "Skill drafts are unavailable in this session."}) + # Tier 2 (ENG-2248): the host wired no draft store; no input can work. + return ToolOutcome( + content=json.dumps({"error": "Skill drafts are unavailable in this session."}), + ok=False, reason="store_unavailable", + ) name = str(tc_input.get("name") or "").strip() if not name: - return json.dumps({"error": "`name` is required."}) + # Tier 2: malformed call. + return ToolOutcome( + content=json.dumps({"error": "`name` is required."}), + ok=False, reason="missing_name", + ) slug = normalize_name(name) if not slug: - return json.dumps({"error": "`name` must contain at least one letter or digit."}) + # Tier 2: malformed call. + return ToolOutcome( + content=json.dumps({"error": "`name` must contain at least one letter or digit."}), + ok=False, reason="invalid_type", + ) folder = Path(root) / slug try: folder.mkdir(parents=True, exist_ok=True) except OSError as exc: logger.warning("skill draft %r: could not claim a folder", slug, exc_info=True) - return json.dumps({"error": f"Could not claim a folder for {slug!r}: {exc}"}) + # Tier 2: the filesystem refused; the draft does not exist. + return ToolOutcome( + content=json.dumps({"error": f"Could not claim a folder for {slug!r}: {exc}"}), + ok=False, reason="store_unavailable", + ) # Only seed an unclaimed folder: a second call in the same turn must not # overwrite what the agent has already written into it. if not (folder / SKILL_FILE).is_file(): _seed_from_store(folder, slug, getattr(session, "_skill_store", None)) - return json.dumps({ + # Tier 1: a draft folder was claimed and its descriptor returned. + return ToolOutcome(content=json.dumps({ "slug": slug, "path": str(folder), "skill_file": str(folder / SKILL_FILE), - }) + }), ok=True) CREATE_SKILL_DRAFT_TOOL = ToolDef( diff --git a/anton/core/tools/tool_handlers.py b/anton/core/tools/tool_handlers.py index 8ecba238..9ee3d74f 100644 --- a/anton/core/tools/tool_handlers.py +++ b/anton/core/tools/tool_handlers.py @@ -2,6 +2,7 @@ import json import logging +import os import uuid from pathlib import Path from typing import TYPE_CHECKING @@ -394,6 +395,46 @@ async def handle_launch_backend(session: "ChatSession", tc_input: dict) -> ToolO tracked = {} session._tracked_backends = tracked + # A subprocess, so its credentials go in its own env — and only for the + # datasources the artifact declared. + vault = getattr(session, "_data_vault", None) + ds_env: dict[str, str] = {} + for ref in artifact.datasources: + if vault is None: + _log.warning("Artifact %s declares datasources but the session has no vault", slug) + break + # Per-ref so one unreadable connection cannot deny the others, and + # env_for is the resolver a pad's own DS_* are built from. + try: + env = vault.env_for(ref.engine, ref.name) + except Exception: + _log.warning( + "Could not resolve %s/%s for backend %s", ref.engine, ref.name, slug, + exc_info=True, + ) + continue + if env is None: + # Declared in metadata but gone from the vault: the backend would + # fail on its first query with nothing saying why. + _log.warning( + "Artifact %s declares %s/%s, which is not in the vault", + slug, ref.engine, ref.name, + ) + continue + # Enforced here, not left to the vault: TurnKeyDataVault.env_for does + # not drop `_`-prefixed bookkeeping though its contract says it does. + field_prefix = f"{ref.env_prefix}__" + for key, value in env.items(): + field = key[len(field_prefix):] if key.startswith(field_prefix) else key + if field.startswith("_"): + continue + ds_env[key] = value + + # Only-if-unset, like the scratchpad, so a project .env cannot override + # PATH or a key this process already has. + overlay = getattr(session, "_workspace_env_overlay", None) or {} + extra_env = {k: v for k, v in overlay.items() if k not in os.environ} + result = await launch_artifact_backend( slug=slug, artifact_folder=store.folder_for(slug), @@ -401,6 +442,8 @@ async def handle_launch_backend(session: "ChatSession", tc_input: dict) -> ToolO tracked_backends=tracked, path=rel_path, extra_args=extra_args, + extra_env=extra_env, + ds_env=ds_env, health_path=health_path, health_timeout=health_timeout, ) @@ -431,7 +474,9 @@ async def handle_launch_backend(session: "ChatSession", tc_input: dict) -> ToolO ).to_outcome() -async def handle_list_artifacts(session: "ChatSession", tc_input: dict) -> str: +async def handle_list_artifacts( + session: "ChatSession", tc_input: dict +) -> "str | ToolOutcome": """List every artifact in the workspace, newest first. Output is a JSON array of summaries — slug, name, type, @@ -443,7 +488,13 @@ async def handle_list_artifacts(session: "ChatSession", tc_input: dict) -> str: store = _artifact_store(session) if store is None: - return "Artifact store unavailable (no workspace bound to this session)." + # Tier 2 (ENG-2248): the tool cannot operate at all, so a retry + # cannot help and repetition is thrash. Reuses the existing + # `store_unavailable` sentinel key (external_wall/service_unavailable). + return ToolOutcome( + content="Artifact store unavailable (no workspace bound to this session).", + ok=False, reason="store_unavailable", + ) artifacts = store.list() summaries = [ @@ -457,10 +508,14 @@ async def handle_list_artifacts(session: "ChatSession", tc_input: dict) -> str: } for a in artifacts ] - return json.dumps(summaries, indent=2) + # Tier 1: a listing was produced. An EMPTY list is still a success — + # "there are no artifacts" is the correct answer, not a failure. + return ToolOutcome(content=json.dumps(summaries, indent=2), ok=True) -async def handle_open_artifact(session: "ChatSession", tc_input: dict) -> str: +async def handle_open_artifact( + session: "ChatSession", tc_input: dict +) -> "str | ToolOutcome": """Load an existing artifact's metadata + folder path. Returns the same shape as `create_artifact` plus the file list @@ -471,13 +526,29 @@ async def handle_open_artifact(session: "ChatSession", tc_input: dict) -> str: store = _artifact_store(session) if store is None: - return "Artifact store unavailable (no workspace bound to this session)." + # Tier 2 (ENG-2248): the tool cannot operate at all, so a retry + # cannot help and repetition is thrash. Reuses the existing + # `store_unavailable` sentinel key (external_wall/service_unavailable). + return ToolOutcome( + content="Artifact store unavailable (no workspace bound to this session).", + ok=False, reason="store_unavailable", + ) slug = (tc_input.get("slug") or "").strip() if not slug: - return "Error: `slug` is required." + # Tier 2: a malformed call; retrying it unchanged cannot work. + return ToolOutcome( + content="Error: `slug` is required.", + ok=False, reason="missing_slug", + ) artifact = store.open(slug) if artifact is None: + # Tier 3 (ENG-2248): deliberately left ok=None. Same shape as + # `recall_skill`'s NO MATCH — the store worked and the artifact simply + # does not exist. The model can list artifacts and pick a real slug, so + # this is arguably its own error; but it is also how a model discovers + # what exists, and ok=False would feed that exploration to the breaker. + # Needs its own decision, not a side effect of this pass. return f"Error: no artifact found for slug `{slug}`." folder = store.folder_for(artifact.slug) # Opening is how the agent gets an artifact's path in order to write to @@ -485,7 +556,8 @@ async def handle_open_artifact(session: "ChatSession", tc_input: dict) -> str: # rather than at write time because the writes themselves happen in # scratchpad cells the tool layer never sees. _track_artifact(session, store, artifact.slug, summary=f"Opened artifact: {artifact.name}") - return json.dumps({ + # Tier 1: the artifact was opened and its descriptor returned. + return ToolOutcome(content=json.dumps({ "id": artifact.id, "slug": artifact.slug, "name": artifact.name, @@ -493,7 +565,7 @@ async def handle_open_artifact(session: "ChatSession", tc_input: dict) -> str: "description": artifact.description, "path": str(folder), "files": [{"path": f.path, "bytes": f.bytes} for f in artifact.files], - }, indent=2) + }, indent=2), ok=True) async def handle_recall(session: ChatSession, tc_input: dict) -> str: @@ -514,7 +586,9 @@ async def handle_recall(session: ChatSession, tc_input: dict) -> str: return session._episodic.recall_formatted(query, **kwargs) -async def handle_memorize(session: ChatSession, tc_input: dict) -> str: +async def handle_memorize( + session: ChatSession, tc_input: dict +) -> "str | ToolOutcome": """Process a memorize tool call and return a result string. Encoding is fire-and-forget so it never blocks scratchpad execution. @@ -522,16 +596,29 @@ async def handle_memorize(session: ChatSession, tc_input: dict) -> str: import asyncio if session._cortex is None: - return "Memory system not available." + # Tier 2 (ENG-2248): no memory system wired, so no entry can ever be + # stored. Retrying cannot help. + return ToolOutcome( + content="Memory system not available.", + ok=False, reason="store_unavailable", + ) if session._cortex.mode == "off": + # Tier 3 (ENG-2248): deliberately ok=None. This is a CONFIGURED state, + # not a failure — the user turned memory off, and the tool reported that + # correctly. Marking it ok=False would nudge and then break the tool for + # every user who has memory disabled on purpose. return "Memory encoding is disabled. Change memory mode via /setup to enable." from anton.core.memory.base import Engram raw_entries = tc_input.get("entries", []) if not raw_entries: - return "No entries provided." + # Tier 2: a malformed call. + return ToolOutcome( + content="No entries provided.", + ok=False, reason="missing_name", + ) engrams: list[Engram] = [] for entry in raw_entries: @@ -559,7 +646,12 @@ async def handle_memorize(session: ChatSession, tc_input: dict) -> str: ) if not engrams: - return "No valid entries provided." + # Tier 2: every entry was rejected by the shape checks above, so the + # call carried nothing usable. + return ToolOutcome( + content="No valid entries provided.", + ok=False, reason="invalid_type", + ) # Always encode immediately via fire-and-forget — the LLM explicitly # chose to memorize these, so we never interrupt the user mid-turn @@ -575,7 +667,10 @@ async def _encode_bg(cortex, entries): session._track_memory_write(asyncio.create_task(_encode_bg(session._cortex, engrams))) descriptions = [f"Encoded {e.kind}: {e.text}" for e in engrams] - return "Memory updated: " + "; ".join(descriptions) + # Tier 1: at least one entry was stored. + return ToolOutcome( + content="Memory updated: " + "; ".join(descriptions), ok=True + ) async def handle_scratchpad( @@ -726,7 +821,7 @@ def _acc_observe(kind: str, detail: dict, *, severity: int = 1) -> None: async def handle_read_image( session: "ChatSession", tc_input: dict -) -> str | list[dict]: +) -> "ToolOutcome": """Read an image file from disk and return it as an image content block. Returns a list of content blocks (image + text) on success so the model @@ -745,7 +840,11 @@ async def handle_read_image( file_path = (tc_input.get("file_path") or "").strip() if not file_path: - return "Error: file_path is required." + # Tier 2 (ENG-2248): a malformed call; a retry cannot fix it. + return ToolOutcome( + content="Error: file_path is required.", + ok=False, reason="missing_name", + ) try: path = Path(file_path).expanduser() @@ -760,21 +859,44 @@ async def handle_read_image( root = Path(base) if base else Path.cwd() path = (root / path).resolve() except OSError as exc: - return f"Error: invalid path '{file_path}': {exc}" + # Tier 2: the model supplied a path that will not parse. + return ToolOutcome( + content=f"Error: invalid path '{file_path}': {exc}", + ok=False, reason="invalid_type", + ) if not path.is_file(): - return f"Error: file not found: {path}" + # Tier 2: the file is genuinely absent. Unlike `recall_skill`'s NO + # MATCH, there is no listing the model can consult to self-correct and + # nothing here tells it to proceed regardless, so repeating the same + # path IS thrash and belongs in the streak. + return ToolOutcome( + content=f"Error: file not found: {path}", + ok=False, reason="path_not_found", + ) if not is_image_path(path.name): - return ( - f"Error: '{path.name}' is not a supported image format " - "(expected .png/.jpg/.jpeg/.gif/.webp/.bmp)." + # Tier 2: the model pointed the image tool at a non-image. Its own + # argument, and the message names the accepted extensions. + return ToolOutcome( + content=( + f"Error: '{path.name}' is not a supported image format " + "(expected .png/.jpg/.jpeg/.gif/.webp/.bmp)." + ), + ok=False, reason="not_an_image", ) try: raw = path.read_bytes() except OSError as exc: - return f"Error: cannot read '{path}': {exc}" + # Tier 2: the read itself failed. `ok=False` is certain — the model got + # no image. The CAUSE is not: this wraps a bare `except Exception`, so + # `read_failed` is mapped TIER_UNCLASSIFIED rather than guessing at a + # permissions wall. + return ToolOutcome( + content=f"Error: cannot read '{path}': {exc}", + ok=False, reason="read_failed", + ) suffix = path.suffix.lstrip(".").lower() if suffix == "bmp": @@ -787,13 +909,24 @@ async def handle_read_image( raw = buf.getvalue() suffix = "png" except Exception as exc: - return f"Error: failed to convert BMP to PNG: {exc}" + # Tier 2 for the verdict, unclassified for the cause: a bare + # `except Exception` around a PIL call covers a missing Pillow (a + # wall) and a corrupt BMP (self-inflicted) with one sentinel. + return ToolOutcome( + content=f"Error: failed to convert BMP to PNG: {exc}", + ok=False, reason="bmp_convert_failed", + ) if len(raw) * 4 // 3 > MAX_IMAGE_BYTES: - return ( - f"Error: image is too large ({human_size(len(raw))}); " - "the API limit is ~3.7 MB raw / 5 MB base64. " - "Resize the image and try again." + # Tier 2: over the API's hard limit. The model chose the file and the + # message tells it what to do instead. + return ToolOutcome( + content=( + f"Error: image is too large ({human_size(len(raw))}); " + "the API limit is ~3.7 MB raw / 5 MB base64. " + "Resize the image and try again." + ), + ok=False, reason="image_too_large", ) b64 = base64.standard_b64encode(raw).decode("ascii") @@ -808,17 +941,32 @@ async def handle_read_image( except Exception: pass - return [ - { - "type": "image", - "source": { - "type": "base64", - "media_type": media_type, - "data": b64, + # Tier 1, and the ONLY multimodal verdict in the tree. `ok=True` is free + # here: the list arms of both tool loops already treat a list result as a + # success unless a handler says otherwise, so this changes the emitted + # `ok` from "unknown" to `true` and nothing else. + # + # It must stay `ok=True`. A list carrying `ok=False` would reach two + # documented gaps in the list arms — the nudge/breaker text is never + # appended there, and `_record_root_cause` is never called — so the model + # would be silently retried past the breaker. `_tool_failure_cause`'s + # docstring in session.py describes that shape as unreachable; it stays + # unreachable because every failure above returns a plain string. + # Pinned by `test_read_image_never_pairs_a_list_with_a_failure_verdict`. + return ToolOutcome( + content=[ + { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": b64, + }, }, - }, - {"type": "text", "text": summary}, - ] + {"type": "text", "text": summary}, + ], + ok=True, + ) # --------------------------------------------------------------------------- diff --git a/anton/core/tools/web_tools.py b/anton/core/tools/web_tools.py index 58ce0a86..9e5d3b98 100644 --- a/anton/core/tools/web_tools.py +++ b/anton/core/tools/web_tools.py @@ -38,7 +38,7 @@ from typing import TYPE_CHECKING, Any from urllib.parse import urlparse -import httpx +import httpx2 as httpx from anton.core.tools.tool_defs import ToolDef diff --git a/anton/core/turn_cost.py b/anton/core/turn_cost.py index bfb1b2d0..4de3e2e4 100644 --- a/anton/core/turn_cost.py +++ b/anton/core/turn_cost.py @@ -36,6 +36,7 @@ from __future__ import annotations +import secrets import time from dataclasses import dataclass, field @@ -142,8 +143,10 @@ class TurnCost: # turns, 3x the tokens of a completed one) had no groupable cause. Values # are the classification the verdict loop already draws for the latch — # `truncated` / `transient` / `hard` / `denied` — plus `latched_hard` / - # `latched_denied` for turns that never made the call because an earlier - # one latched. Empty when a verdict was produced or the verifier was + # `latched_truncated` / `latched_mixed` / `latched_denied` for turns that + # never made the call because an earlier one latched. `latched_mixed` means + # the counted failures were not all the same class, so neither names the + # cause on its own. Empty when a verdict was produced or the verifier was # not applicable. Stamped at the loop's exits, never read from latch # state at emit (late finalizers would see a later turn's latch). verifier_failure: str = "" @@ -155,6 +158,46 @@ class TurnCost: # needs. Empty when no exception ended the verdict (verdict produced, or # latched skip with no call made). verifier_error_type: str = "" + # WHY the retry flow terminated, when a turn ended after retrying (ENG-1361). + # Named for TERMINATION, not exhaustion: `rate_limit_wait_too_long` is a + # terminal where nothing ran out — the server named an interval past our cap + # and we declined to wait — so an "exhaustion" name would be a lie on a + # quarter of its own values. + # + # It exists because ENG-1361 makes the request-time and mid-stream terminals + # raise the SAME ProviderOverloadedError, and `code` (which splits the + # rate-limit case) is a card contract that never reaches analytics. Without + # this, three distinct outcomes become one indistinguishable bucket. Note + # the retry count itself is a local in `turn_stream` and reaches nothing — + # there is no other telemetry saying a turn retried at all. + # "request_attempt_limit" — the count-based attempt budget ran out + # "provider_recovery_timeout" — the mid-stream incident budget ran out + # "rate_limit_wait_limit" — the rate-limit wait allowance ran out + # "rate_limit_wait_too_long" — Retry-After exceeded our cap; we carded + # immediately rather than stalling the turn + # Empty for every terminal that did not retry. + retry_terminal_reason: str = "" + # WHAT kind of provider failure ended the turn — a closed vocabulary + # (`PROVIDER_FAILURE_KINDS`), NOT the raw exception `code` (ENG-1361). + # `code` selects the client's card and mixes cardinalities (`http_503` + # beside `connection_error`), so it is unfit for a groupable analytics + # dimension. Empty when the failure was not a provider failure. + provider_failure_kind: str = "" + # The HTTP status of the failure that ended the turn, whenever it carried + # one. `int | None`, never "" — a mid-stream failure (status 200) and a + # connection error (no response) are genuinely ABSENT, and an empty string + # beside integers is the shape that makes an analytics column unqueryable. + # Omitted from the event entirely when None, rather than sent as a + # placeholder. + # + # INDEPENDENT of `provider_failure_kind`, deliberately. The kind classifies + # PROVIDER failures and is empty for anything else; the status is a plain + # fact about the exception. So a non-transient terminal (an SDK + # `BadRequestError` exhausting the attempt budget) books + # kind="" with status=400 — not a hole, just the two fields answering two + # different questions. Suppressing a real status to keep the pair + # symmetrical would discard the only signal those turns carry. + provider_http_status: int | None = None started_monotonic: float = field(default_factory=time.monotonic) # Set when these books have been reported. Replaces "the shared slot is # None" as the double-emit guard, because a late finalizer now emits the @@ -166,6 +209,38 @@ class TurnCost: # there gave the abandoned turn a LATER, unrelated turn's index — the # cost→Langfuse hop then pointed at the wrong turn (#309 review follow-up). turn_index: int = 0 + # Unique per turn EXECUTION. `turn_index` is only a POSITION in the history + # — `_turn_count` is seeded by counting the user messages the session was + # handed — and cowork-server rebuilds the session every turn, so a retried + # or cancelled attempt arrives with the same history and stamps the same + # `turn_index` (ENG-2243). Measured on prod 2026-08-28..09-01: that collided + # on 14.5% of desktop turn keys (worst: 16 rows on one key, spanning 34 + # hours) and left 18.5% of `tool_completed` rows joining to more than one + # `turn_completed` row — the join ENG-1486 stamped the pair to enable. + # + # Random, not a counter, deliberately: ANY session-local counter resets on + # that same rebuild, so nothing derived from session state can be unique + # across attempts. `token_hex(8)` is 16 hex chars of 64 real bits — + # collision-free at our volume (the birthday bound is ~5e9 attempts), short + # enough to read in a log line. + # + # NOT `uuid.uuid4().hex[:16]`, which this field shipped as in review and + # which is 60 bits, not 64: hex position 12 is uuid4's version nibble, so + # it is the literal `4` in every id ever generated (measured 2000/2000). + # The width claim was wrong and so was the comparison to the `aid` install + # fingerprint: `get_installation_id()` in `anton/analytics.py` takes the + # `sha256(str(node)).hexdigest()[:16]` branch whenever the machine has a + # real MAC, which is genuinely 64 bits; its `uuid4().hex[:16]` branch is + # the fallback for a host with no MAC to read (Docker with stripped + # networking), and carries the same 60-bit shortfall described above. + # + # A `default_factory` rather than a value passed in at each construction + # site: there are two today (the streaming and non-streaming turns in + # `session.py`) and a third that forgot would silently reintroduce the + # collision this field exists to remove. Also makes it a stamped-at-open + # fact like `turn_index` above, so a late finalizer reports the id of the + # turn whose books it holds rather than whichever turn owns the slot now. + attempt_id: str = field(default_factory=lambda: secrets.token_hex(8)) # When the turn ended. None until resolved. A late finalizer cannot know the # real end, so it falls back to `last_activity_monotonic` (the last LLM # call) — understating but bounded, rather than measuring up to whenever diff --git a/anton/core/yolo/__init__.py b/anton/core/yolo/__init__.py new file mode 100644 index 00000000..f52b1e7f --- /dev/null +++ b/anton/core/yolo/__init__.py @@ -0,0 +1,76 @@ +"""Yolo mode: edit files with a diff instead of a program that writes one. + +Anton's existing way to change an artifact is the scratchpad — the model +writes Python that writes the file. That works, and for anything +generative it is the right tool. For *modifying* a file that already +exists it is a long way round: the model reasons about string surgery, +writes a program to perform it, and the program is a second thing that can +be wrong. A one-line title change becomes a script. + +Yolo mode is the short way. The model is shown the folder, asks for the +files it needs, and returns a diff. The diff is applied here, by finding +each hunk's text in the file — no line numbers involved. + +What was learned building yolocoder, and what this preserves: + +* **Models get diff content right and diff arithmetic wrong.** Line + numbers, hunk counts and trailing context are the failure, not the + edit. So all of it is ignored and hunks are placed by their text. + This is the single highest-value idea here. +* **Models pick files well.** Given a listing of thirty paths they ask + for the right two or three. That judgement is worth one cheap call and + is not worth second-guessing. +* **Plan and patch belong in one call.** Split, the second call resends + every file to learn nothing. +* **Failure evidence must include the model's own output.** Told only + "it failed", a model reproduces the same diff. +* **Refusing beats guessing.** An unfindable hunk, or a short one + matching in three places, is an error — not a reason to pick the + first match and edit the wrong function. + +`patch.py` and `workspace.py` import nothing at all — they are pure +functions over strings, which is what makes the engine provable in +milliseconds. `agent.py` uses anton's own `LLMClient` directly, so a yolo +run gets the configured provider, the coding-model split, forced +`tool_choice`, pydantic validation and turn tracing for free, and the +handler passes the same client the rest of the agent already holds. + + from anton.core.yolo import Workspace, YoloEditor + + editor = YoloEditor(workspace=Workspace(folder), llm_client=llm_client) + outcome = await editor.edit("rename the title to TicTacTris") + if not outcome.applied: + ... # outcome.detail says why; fall back to the scratchpad +""" + +from anton.core.yolo.agent import MAX_ATTEMPTS, YoloEditor, apply_patch_text +from anton.core.yolo.models import Change, Outcome, Progress, ReadRequest +from anton.core.yolo.patch import ( + FilePatch, + Hunk, + PatchError, + apply_hunks, + is_apply_patch_format, + locate, + parse_patch, +) +from anton.core.yolo.workspace import Workspace, WorkspaceError + +__all__ = [ + "MAX_ATTEMPTS", + "Change", + "FilePatch", + "Hunk", + "Outcome", + "PatchError", + "Progress", + "ReadRequest", + "Workspace", + "WorkspaceError", + "YoloEditor", + "apply_hunks", + "apply_patch_text", + "is_apply_patch_format", + "locate", + "parse_patch", +] diff --git a/anton/core/yolo/agent.py b/anton/core/yolo/agent.py new file mode 100644 index 00000000..48f25db0 --- /dev/null +++ b/anton/core/yolo/agent.py @@ -0,0 +1,374 @@ +"""The yolo loop: pick files, write one diff, place it, repair, give up well. + +Shape of a run, and the reasoning behind each step: + + map ──▶ pick files ──▶ read ──▶ plan + diff ──▶ apply ─┬─▶ done + + search (1 call) │ + (1 call) ▲ │ + └── evidence ◀───┘ + widen / search + (up to MAX_ATTEMPTS) + +Two LLM calls for a change that lands, and the second one is the only one +that repeats. Everything else — searching, locating hunks, checking that +promised files exist, deciding whether a match is ambiguous — is +arithmetic done here, because it is arithmetic and models are bad at it. + +Searching in particular costs no model call at all, which is why the +model may ask for it freely: at the pick step when file names give +nothing away, and again on a failed attempt when it turns out to have +been looking in the wrong file. + +Giving up is a designed step, not a fallthrough. When the diff will not +apply after MAX_ATTEMPTS, the outcome carries the last failure verbatim so +the caller can hand the job to something else with the diagnosis attached. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from anton.core.llm.client import LLMClient +from anton.core.yolo.models import Change, Outcome, Progress, ReadRequest +from anton.core.yolo.patch import PatchError, apply_hunks, parse_patch +from anton.core.yolo.prompts import ( + CHANGE_INSTRUCTIONS, + READ_INSTRUCTIONS, + render_task, +) +from anton.core.yolo.workspace import ( + SCHEMA_SUFFIX, + Workspace, + WorkspaceError, + is_generated_data, +) + +__all__ = ["MAX_ATTEMPTS", "YoloEditor", "apply_patch_text"] + + +# How many diffs to accept before handing the job on. Three is not +# arbitrary: in yolocoder, a diff that fails three times with the file +# contents already in front of the model is almost never fixed by a +# fourth — the model is wrong about the file, not careless, and more +# attempts spend tokens to arrive at the same place. +MAX_ATTEMPTS = 3 + +# A read budget. Models pick files well but will happily ask for the whole +# folder if the folder is small enough to seem free. +MAX_READS = 12 + + +class _Quiet: + def status(self, message: str) -> None: ... + def log(self, message: str) -> None: ... + + +@dataclass +class YoloEditor: + """Makes one change to one folder.""" + + workspace: Workspace + llm_client: LLMClient + progress: Progress = field(default_factory=_Quiet) + max_attempts: int = MAX_ATTEMPTS + + async def edit(self, task: str, background: str = "") -> Outcome: + """Make the change, or come back saying exactly why not.""" + try: + return await self._edit(task, background) + except WorkspaceError as error: + return Outcome(applied=False, detail=str(error), status="error") + + async def _edit(self, task: str, background: str) -> Outcome: + file_map = self.workspace.map() + self.progress.status("Looking at the folder...") + self.progress.log(f" {len(self.workspace.files())} files") + + wanted, found = await self._pick_files(task, file_map, background) + contents = "" + if wanted: + self.progress.log(" reading " + ", ".join(wanted)) + contents = self.workspace.read_many(wanted) + + evidence = "" + change: Change | None = None + + for attempt in range(1, self.max_attempts + 1): + self.progress.status( + "Working out the change..." if attempt == 1 else "Trying again..." + ) + change = await self.llm_client.generate_object_code( + Change, + system=CHANGE_INSTRUCTIONS, + messages=[ + { + "role": "user", + "content": render_task( + task, file_map, contents, background, evidence, found + ), + } + ], + ) + if attempt == 1: + self.progress.log(f" plan: {change.summary}") + + if not change.diff.strip(): + # "I need to see more first" is a legitimate answer, not a + # dead end. Honour it rather than treating an empty diff as + # a failure to produce one. + widened, extra = self._widen(wanted, change) + if (widened != wanted or extra) and attempt < self.max_attempts: + wanted, contents = widened, self.workspace.read_many(widened) + found = _join(found, extra) + evidence = _asked_note(change.need_files, change.need_search) + self.progress.log( + " asked for " + + ", ".join(change.need_files + change.need_search) + ) + continue + # Otherwise there is nothing to apply and nothing to repair + # from; another attempt asks the same question again. + return Outcome( + applied=False, + summary=change.summary, + detail="the model returned no diff", + attempts=attempt, + status="no_diff", + ) + + self.progress.status("Applying the patch...") + try: + written = apply_patch_text(self.workspace, change.diff) + except (PatchError, WorkspaceError) as error: + self.progress.log(f" patch did not apply: {error}") + # A wrong first pick is the one failure the repair note + # cannot fix. Telling a model to copy the lines more + # carefully is useless advice about a file it was never + # shown, and without widening the read set it will rewrite + # the same doomed diff until the attempts run out. So when + # the patch named a file we did not read, that — not the + # careless-copying lecture — is the evidence to send back. + widened, extra = self._widen(wanted, change) + fresh = [path for path in widened if path not in wanted] + if fresh or extra: + if fresh: + self.progress.log(" reading " + ", ".join(fresh) + " and retrying") + wanted, contents = widened, self.workspace.read_many(widened) + found = _join(found, extra) + evidence = _unseen_note(fresh, str(error)) + else: + evidence = _repair_note(str(error), change.diff) + continue + + # The model said which files it would touch. That claim is + # worth checking, because a patch that quietly omits the new + # file it promised applies perfectly and looks like success. + missing = [ + path + for path in change.files + if not self.workspace.exists(path) and path not in written + ] + if missing: + self.progress.log(f" but {', '.join(missing)} was not created") + evidence = _missing_note(missing) + # This patch did apply, so the files on disk are no longer + # the ones quoted in the prompt. Re-read them, or the next + # attempt writes a diff against a version that is gone and + # fails for a reason that has nothing to do with the task. + wanted = _merge(wanted, sorted(written)) + contents = self.workspace.read_many(wanted) + continue + + self.progress.log(f" wrote {', '.join(sorted(written))}") + return Outcome( + applied=True, + summary=change.summary, + files=sorted(written), + attempts=attempt, + status="applied", + ) + + return Outcome( + applied=False, + summary=change.summary if change else "", + files=list(change.files) if change else [], + detail=evidence, + attempts=self.max_attempts, + status="patch_failed", + ) + + def _widen(self, wanted: list[str], change: Change) -> tuple[list[str], str]: + """Widen the read set from what the model named or asked to find. + + This is the bounded stand-in for a read/search tool loop. The loop + in yolocoder was not really about the first pick — models are good + at that — it was about recovering when the first pick was wrong. + Removing it left nothing to recover with. + + Rather than reopen an unbounded loop, the read set widens only in + reaction to a failure, only to files the model itself named or + found, and only within the attempts already budgeted. Searching is + free — it calls no model — so it costs nothing to answer. + + Returns the new read set and any search results to show. + """ + widened = list(wanted) + rendered, hits = "", [] + if change.need_search: + rendered, hits = self.workspace.search_many(change.need_search) + for path in list(change.need_files) + hits + list(change.files): + path = path.strip().lstrip("./") + if path and path not in widened and self.workspace.exists(path): + widened.append(path) + return widened[:MAX_READS], rendered + + async def _pick_files( + self, task: str, file_map: str, background: str + ) -> tuple[list[str], str]: + """Ask which files the change needs, and search for what it cannot name. + + This is the step models are reliably good at, which is why it gets + its own cheap call instead of a tool loop: one request, a list of + names, done. Anything they name that is not in the folder is + dropped here rather than becoming a confusing read error. + + Where names are not enough — forty files and no clue which one + sets the title — the same call can ask for a search instead. The + search itself is deterministic and costs no model call, so the + whole discovery step stays at one request. Files that match are + added to the read set, and the matching lines are shown so the + model can see why they are there. + """ + self.progress.status("Choosing what to read...") + request = await self.llm_client.generate_object_code( + ReadRequest, + system=READ_INSTRUCTIONS, + messages=[ + { + "role": "user", + "content": render_task(task, file_map, background=background), + } + ], + ) + rendered, hits = "", [] + if request.search: + self.progress.log(" searching for " + ", ".join(request.search)) + rendered, hits = self.workspace.search_many(request.search) + + known = {info.path for info in self.workspace.files()} + wanted, seen = [], set() + for path in list(request.paths) + hits: + path = path.strip().lstrip("./") + if path in known and path not in seen: + seen.add(path) + wanted.append(path) + return wanted[:MAX_READS], rendered + + +def apply_patch_text(workspace: Workspace, diff: str) -> set[str]: + """Apply a whole patch, all files or none. + + Every file's new contents are worked out before anything is written, + so a hunk that will not place in the third file does not leave the + first two edited and the change half-done. Returns the paths written. + """ + patches = parse_patch(diff) + updated: dict[str, str] = {} + + for file_patch in patches: + if not file_patch.hunks: + continue + # A patch may touch one file in several blocks — models happily + # emit two "*** Begin Patch" sections for the same path. Each has + # to build on the last, or the final block silently discards + # everything before it. + if file_patch.path in updated: + current = updated[file_patch.path] + elif workspace.exists(file_patch.path): + current = workspace.read(file_patch.path) + else: + current = "" # a new file: hunks are pure insertions + try: + updated[file_patch.path] = apply_hunks(current, file_patch.hunks) + except PatchError as error: + raise PatchError(f"{file_patch.path}: {error}") from error + + if not updated: + raise PatchError("the patch changed nothing") + + # Generated data belongs to whatever produced it. A diff against two + # megabytes of rows is not an edit anyone reviewed, and regenerating + # the file is both correct and cheaper. Enforced rather than merely + # instructed, for the same reason an ambiguous hunk is refused: the + # rule is checkable here, so it should not depend on the model + # remembering it. + generated = sorted(path for path in updated if is_generated_data(path)) + if generated: + raise PatchError( + f"{', '.join(generated)} is generated data and is not edited by hand. " + f"Change whatever produces it and let it be written again. To use it, " + f"read its {SCHEMA_SUFFIX} sidecar and write code against the global it names." + ) + + for path, content in updated.items(): + workspace.write(path, content) + return set(updated) + + +def _merge(existing: list[str], extra: list[str]) -> list[str]: + """Add paths to the read set, keeping order and dropping duplicates.""" + merged = list(existing) + for path in extra: + if path not in merged: + merged.append(path) + return merged[:MAX_READS] + + +def _repair_note(error: str, diff: str) -> str: + """What to tell the model after a diff would not apply. + + It gets both the complaint and the diff back. Without seeing its own + output a model has no way to tell what was wrong with it, and + reproduces it almost verbatim — the single most common way a repair + loop burns three attempts achieving nothing. + """ + return ( + "Your diff did not apply. Hunks are placed by finding their text in the file, so " + "line numbers were not the problem — the context or removed lines did not match " + "the file exactly. Compare the lines below against the file contents you were " + "shown and copy them character for character, including indentation, quoting and " + "HTML entities. If a hunk was called ambiguous, give it more surrounding context.\n\n" + f"WHAT FAILED:\n{error}\n\nTHE DIFF THAT FAILED:\n{diff}" + ) + + +def _unseen_note(fresh: list[str], error: str) -> str: + """What to say when the diff failed against a file we had not read.""" + return ( + f"Your diff did not apply, and it touched {', '.join(fresh)} — which you had not " + f"been shown when you wrote it. The contents are included above now. Write the " + f"change again against what is actually there.\n\nWHAT FAILED:\n{error}" + ) + + +def _asked_note(files: list[str], queries: list[str]) -> str: + asked = ", ".join(files + [f'"{query}"' for query in queries]) + return ( + f"You asked about {asked} before writing the change. What was found is above. " + f"Now write it." + ) + + +def _join(existing: str, extra: str) -> str: + """Keep earlier search results alongside later ones.""" + return "\n".join(block for block in (existing, extra) if block.strip()) + + +def _missing_note(missing: list[str]) -> str: + return ( + f"The patch applied but did not create {', '.join(missing)}, which you listed as " + "files it touches. A file that does not exist yet must be created by the patch " + 'itself: use "*** Add File: " followed by every line of its contents each ' + 'prefixed with "+", or a unified diff whose header is "--- /dev/null". Include the ' + "complete contents, not a description of them." + ) diff --git a/anton/core/yolo/data.py b/anton/core/yolo/data.py new file mode 100644 index 00000000..41560dc5 --- /dev/null +++ b/anton/core/yolo/data.py @@ -0,0 +1,270 @@ +"""Generated data files and the schemas that describe them. + +A data file is one line: + + window.ANTON_DATA_prices = [{"date": "2026-08-31", "price": 41.22}]; + +That shape is chosen so it can be two things at once. A browser loads it +with a plain `\n") + workspace.write("chart.js", "import { fmt } from './utils.js';\n") + workspace.write("utils.js", "export const fmt = (n) => n.toFixed(2);\n") + return workspace + + +async def test_a_failed_diff_against_an_unread_file_widens_the_read_set( + tmp_path: Path, +): + """The first pick missed utils.js. The model writes against it anyway + and fails. It must be shown the file, not lectured about copying.""" + workspace = three_file_project(tmp_path) + blind = Change( + summary="round to 0 dp", + files=["utils.js"], + diff="--- a/utils.js\n+++ b/utils.js\n@@\n" + "-export const fmt = (n) => n.toFixed(1);\n" # wrong — never saw it + "+export const fmt = (n) => n.toFixed(0);\n", + ) + sighted = Change( + summary="round to 0 dp", + files=["utils.js"], + diff="--- a/utils.js\n+++ b/utils.js\n@@\n" + "-export const fmt = (n) => n.toFixed(2);\n" + "+export const fmt = (n) => n.toFixed(0);\n", + ) + coder = StubCoder(ReadRequest(paths=["chart.js"]), blind, sighted) + outcome = await YoloEditor(workspace=workspace, llm_client=coder).edit("round it") + + assert outcome.applied + assert outcome.attempts == 2 + retry = coder.calls[2]["content"] + # utils.js is now in front of it, with its real contents. + assert "toFixed(2)" in retry + assert "had not been shown" in retry + assert "toFixed(0)" in workspace.read("utils.js") + + +async def test_the_model_can_ask_for_files_instead_of_guessing(tmp_path: Path): + """An empty diff plus need_files is a legitimate answer, not a dead + end. Previously it ended the run as `no_diff`.""" + workspace = three_file_project(tmp_path) + asking = Change(summary="", files=[], diff="", need_files=["utils.js"]) + answering = Change( + summary="round to 0 dp", + files=["utils.js"], + diff="--- a/utils.js\n+++ b/utils.js\n@@\n" + "-export const fmt = (n) => n.toFixed(2);\n" + "+export const fmt = (n) => n.toFixed(0);\n", + ) + coder = StubCoder(ReadRequest(paths=["chart.js"]), asking, answering) + outcome = await YoloEditor(workspace=workspace, llm_client=coder).edit("round it") + + assert outcome.applied, "asking for a file should not end the run" + assert outcome.attempts == 2 + assert "toFixed(2)" in coder.calls[2]["content"] + + +async def test_asking_for_a_file_that_does_not_exist_still_ends_the_run( + tmp_path: Path, +): + """Widening only ever adds real files, so an invented name cannot loop.""" + workspace = three_file_project(tmp_path) + coder = StubCoder( + ReadRequest(paths=["chart.js"]), + Change(summary="", files=[], diff="", need_files=["imaginary.js"]), + ) + outcome = await YoloEditor(workspace=workspace, llm_client=coder).edit("x") + assert outcome.status == "no_diff" + assert len(coder.calls) == 2 + + +async def test_widening_cannot_run_past_the_attempt_budget(tmp_path: Path): + """Recovery is bounded by the same 3 attempts — it is not a loop.""" + workspace = three_file_project(tmp_path) + asking = Change(summary="", files=[], diff="", need_files=["utils.js", "index.html"]) + coder = StubCoder(ReadRequest(paths=[]), asking, asking, asking) + outcome = await YoloEditor(workspace=workspace, llm_client=coder).edit("x") + + assert not outcome.applied + # 1 pick + at most 3 changes. It cannot keep asking forever. + assert len(coder.calls) <= 4 + + +async def test_a_plain_bad_diff_still_gets_the_copy_carefully_note(tmp_path: Path): + """Widening must not swallow the ordinary case. When every named file + was already read, the failure really is careless copying.""" + workspace = three_file_project(tmp_path) + bad = Change( + summary="x", + files=["chart.js"], + diff="--- a/chart.js\n+++ b/chart.js\n@@\n-NOT IN THE FILE\n+y\n", + ) + coder = StubCoder(ReadRequest(paths=["chart.js"]), bad, bad, bad) + await YoloEditor(workspace=workspace, llm_client=coder).edit("x") + + retry = coder.calls[2]["content"] + assert "character for character" in retry + assert "had not been shown" not in retry diff --git a/tests/test_yolo_data.py b/tests/test_yolo_data.py new file mode 100644 index 00000000..171a5650 --- /dev/null +++ b/tests/test_yolo_data.py @@ -0,0 +1,194 @@ +"""Generated data files, and why their schemas cannot drift. + +The design question this settles: how do you keep a `.data.js` and its +`.schema.json` in step when anything might rewrite either? + +The answer is not to try. The schema is *derived* from the bytes on disk +and can be re-derived at any moment, so nothing is ever asked to keep +them in step — `reconcile()` just recomputes. That removes the need for +one blessed way of producing data, which was the alternative. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from anton.core.yolo import Workspace +from anton.core.yolo.data import ( + DataError, + derive_schema, + global_name, + read_data, + reconcile, + write_data, +) + +ROWS = [ + {"date": "2026-08-31", "price": 41.22, "volume": 19773, "flagged": False}, + {"date": "2026-09-01", "price": 41.90, "volume": None, "flagged": True}, +] + + +def schema_of(workspace: Workspace, name: str = "prices") -> dict: + return json.loads(workspace.read(f"{name}.schema.json")) + + +# ─── The file is two things at once ───────────────────────────────────── + + +def test_a_data_file_is_loadable_by_a_script_tag(tmp_path: Path): + """The reason it is .js and not .json: an artifact opened from disk + cannot fetch() a sibling file, so the data has to arrive as a global. + The same file works unchanged when published over HTTP.""" + workspace = Workspace(tmp_path) + write_data(workspace, "prices", ROWS) + text = workspace.read("prices.data.js") + assert text.startswith("window.ANTON_DATA_prices = ") + assert text.rstrip().endswith(";") + + +def test_the_same_file_is_readable_without_a_javascript_engine(tmp_path: Path): + """The reason it is not awkward: the value is a JSON literal behind a + fixed prefix, so anything can read it back.""" + workspace = Workspace(tmp_path) + write_data(workspace, "prices", ROWS) + assert read_data(workspace, "prices.data.js") == ROWS + + +def test_a_data_file_in_the_wrong_shape_says_so(tmp_path: Path): + workspace = Workspace(tmp_path) + workspace.write("bad.data.js", "const x = [1,2,3];\n") + with pytest.raises(DataError, match="expected"): + read_data(workspace, "bad.data.js") + + +def test_a_data_file_holding_broken_json_says_so(tmp_path: Path): + workspace = Workspace(tmp_path) + workspace.write("bad.data.js", "window.ANTON_DATA_bad = [1,2,;\n") + with pytest.raises(DataError, match="valid JSON"): + read_data(workspace, "bad.data.js") + + +def test_awkward_names_still_make_a_legal_global(tmp_path: Path): + assert global_name("q3-sales report") == "ANTON_DATA_q3_sales_report" + + +# ─── The schema is derived, never stated ──────────────────────────────── + + +def test_types_come_from_the_values_not_from_a_description(tmp_path: Path): + """The failure this prevents: a hand-written {"price": "number"} over + rows that actually hold "1,234.00" strings. Code written against that + renders a broken chart rather than raising.""" + schema = derive_schema("prices", [{"price": "1,234.00"}]) + assert schema["fields"]["price"]["type"] == "string" + + +def test_a_boolean_is_not_reported_as_a_number(tmp_path: Path): + """True is an int in Python. Calling it a number sends whoever reads + the schema looking for arithmetic on a flag.""" + assert derive_schema("f", [{"ok": True}])["fields"]["ok"]["type"] == "boolean" + + +def test_nullability_is_observed(tmp_path: Path): + fields = derive_schema("prices", ROWS)["fields"] + assert fields["volume"]["nullable"] is True + assert "nullable" not in fields["price"] + + +def test_a_column_with_mixed_types_admits_it(tmp_path: Path): + schema = derive_schema("m", [{"x": 1}, {"x": "two"}]) + assert schema["fields"]["x"]["type"] == "number | string" + + +def test_a_column_missing_from_some_rows_is_still_described(tmp_path: Path): + schema = derive_schema("m", [{"a": 1}, {"a": 2, "b": "late"}]) + assert set(schema["fields"]) == {"a", "b"} + assert schema["fields"]["b"]["nullable"] is True + + +def test_the_schema_names_the_global(tmp_path: Path): + """Perfect knowledge of the columns is useless if the chart cannot + find the array.""" + workspace = Workspace(tmp_path) + write_data(workspace, "prices", ROWS) + assert schema_of(workspace)["global"] == "ANTON_DATA_prices" + + +def test_notes_are_the_only_thing_asked_for(tmp_path: Path): + """Units, gaps and timezones cannot be inferred from the values, and + they are what produce plausible-looking wrong charts.""" + workspace = Workspace(tmp_path) + write_data(workspace, "prices", ROWS, notes="Daily close. Gaps on weekends.") + assert "Gaps on weekends" in schema_of(workspace)["notes"] + + +# ─── Reconciliation: drift is impossible, not merely discouraged ──────── + + +def test_a_missing_sidecar_is_written(tmp_path: Path): + """Data produced some other way — by hand, by an older cell — still + ends up described. This is what removes the need for one blessed + way of writing a data file.""" + workspace = Workspace(tmp_path) + workspace.write("prices.data.js", 'window.ANTON_DATA_prices = [{"a": 1}];') + + report = reconcile(workspace) + assert any("written" in line for line in report) + assert schema_of(workspace)["fields"]["a"]["type"] == "number" + + +def test_a_stale_sidecar_is_refreshed(tmp_path: Path): + workspace = Workspace(tmp_path) + write_data(workspace, "prices", ROWS) + # Something else rewrites the data, with a different shape entirely. + workspace.write("prices.data.js", 'window.ANTON_DATA_prices = [{"date":"x","price":"1,234.00"}];') + + report = reconcile(workspace) + assert any("refreshed" in line for line in report) + assert schema_of(workspace)["fields"]["price"]["type"] == "string" + assert schema_of(workspace)["rows"] == 1 + + +def test_reconciling_preserves_the_one_field_nobody_can_derive(tmp_path: Path): + workspace = Workspace(tmp_path) + write_data(workspace, "prices", ROWS, notes="Daily close. Gaps on weekends.") + workspace.write("prices.data.js", 'window.ANTON_DATA_prices = [{"date":"x"}];') + + reconcile(workspace) + assert "Gaps on weekends" in schema_of(workspace)["notes"] + + +def test_an_unchanged_sidecar_is_left_alone(tmp_path: Path): + """Rewriting it every time would churn the file and its timestamp for + no reason, and make every reconcile look like a change.""" + workspace = Workspace(tmp_path) + write_data(workspace, "prices", ROWS) + before = workspace.read("prices.schema.json") + assert reconcile(workspace) == [] + assert workspace.read("prices.schema.json") == before + + +def test_unreadable_data_is_reported_rather_than_skipped(tmp_path: Path): + workspace = Workspace(tmp_path) + workspace.write("mystery.data.js", "this is not a data file at all\n") + [line] = reconcile(workspace) + assert "cannot read it back" in line + + +def test_a_corrupt_sidecar_is_simply_rewritten(tmp_path: Path): + workspace = Workspace(tmp_path) + write_data(workspace, "prices", ROWS) + workspace.write("prices.schema.json", "{ not json") + assert any("written" in line for line in reconcile(workspace)) + assert schema_of(workspace)["global"] == "ANTON_DATA_prices" + + +def test_an_orphan_sidecar_is_flagged(tmp_path: Path): + workspace = Workspace(tmp_path) + workspace.write("gone.schema.json", '{"global": "ANTON_DATA_gone"}') + [line] = reconcile(workspace) + assert "not there" in line diff --git a/tests/test_yolo_data_convention.py b/tests/test_yolo_data_convention.py new file mode 100644 index 00000000..82c15be9 --- /dev/null +++ b/tests/test_yolo_data_convention.py @@ -0,0 +1,130 @@ +"""The boundary between the scratchpad's files and yolo's. + +`.data.js` is produced by whatever computes it — the scratchpad — +and `.schema.json` sits beside it saying what the columns are and, +critically, what global the data file defines. + +These tests pin the two halves of the contract: the schema is always put +in front of the model, and the data file is never edited by it. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from anton.core.yolo import PatchError, Workspace, apply_patch_text +from anton.core.yolo.workspace import ( + DATA_SUFFIX, + SCHEMA_SUFFIX, + is_generated_data, + is_schema, + schema_for, +) + +SCHEMA = { + "global": "ANTON_DATA_prices", + "file": "prices.data.js", + "shape": "array", + "rows": 8432, + "fields": {"date": {"type": "string"}, "price": {"type": "number", "unit": "USD"}}, + "notes": "Daily close. Gaps on weekends — do not interpolate.", +} + + +def artifact(tmp_path: Path) -> Workspace: + workspace = Workspace(tmp_path) + workspace.write("index.html", "
\n") + workspace.write("chart.js", "// draws the chart\n") + # A data file far too large to ever inline. + workspace.write("prices.data.js", "window.ANTON_DATA_prices=[" + "0," * 50000 + "];\n") + workspace.write("prices.schema.json", json.dumps(SCHEMA, indent=2)) + return workspace + + +def test_the_naming_convention_classifies_files(): + assert is_generated_data("prices" + DATA_SUFFIX) + assert not is_generated_data("chart.js") # an ordinary .js is yolo's + assert is_schema("prices" + SCHEMA_SUFFIX) + assert schema_for("reports/q3.data.js") == "reports/q3.schema.json" + + +def test_the_map_inlines_the_schema_and_never_the_data(tmp_path: Path): + """The whole point. A line reading `prices.data.js (2.1 MB)` tells the + model nothing it can write code against; its sidecar tells it + everything, for a few hundred bytes.""" + file_map = artifact(tmp_path).map() + + # The schema is there in full, including the part that actually + # matters — the global the data file defines. + assert "ANTON_DATA_prices" in file_map + assert "do not interpolate" in file_map + # The data file is listed but its contents never appear. + assert "prices.data.js" in file_map + assert "0,0,0,0,0" not in file_map + # And the listing points from one to the other. + assert "see prices.schema.json" in file_map + + +def test_a_data_file_with_no_sidecar_says_so(tmp_path: Path): + """Silence would read as 'this file is unimportant'. It is not — it is + unusable, and that is worth saying.""" + workspace = Workspace(tmp_path) + workspace.write("orphan.data.js", "window.X=[1,2,3];\n") + assert "no schema sidecar" in workspace.map() + + +def test_yolo_refuses_to_edit_generated_data(tmp_path: Path): + """Enforced, not merely instructed. A diff against two megabytes of + rows is not a change anyone reviewed.""" + workspace = artifact(tmp_path) + before = workspace.read("prices.data.js") + + with pytest.raises(PatchError, match="generated data"): + apply_patch_text( + workspace, + "--- a/prices.data.js\n+++ b/prices.data.js\n@@\n" + "-window.ANTON_DATA_prices=[" + "0," * 50000 + "];\n" + "+window.ANTON_DATA_prices=[1];\n", + ) + assert workspace.read("prices.data.js") == before + + +def test_the_refusal_says_what_to_do_instead(tmp_path: Path): + """The message is fed back to the model, so it has to be actionable.""" + workspace = artifact(tmp_path) + with pytest.raises(PatchError) as caught: + apply_patch_text( + workspace, + "*** Begin Patch\n*** Add File: new.data.js\n+window.X=[];\n*** End Patch\n", + ) + message = str(caught.value) + assert "let it be written again" in message + assert SCHEMA_SUFFIX in message + + +def test_a_mixed_patch_writes_nothing(tmp_path: Path): + """One legitimate file and one data file in the same patch is still a + refusal — all files or none, as everywhere else.""" + workspace = artifact(tmp_path) + with pytest.raises(PatchError, match="generated data"): + apply_patch_text( + workspace, + "--- a/chart.js\n+++ b/chart.js\n@@\n-// draws the chart\n+// updated\n" + "*** Begin Patch\n*** Add File: extra.data.js\n+window.Y=[];\n*** End Patch\n", + ) + assert workspace.read("chart.js") == "// draws the chart\n" + + +def test_code_beside_data_is_still_editable(tmp_path: Path): + """The convention must not make ordinary .js files untouchable.""" + workspace = artifact(tmp_path) + written = apply_patch_text( + workspace, + "--- a/chart.js\n+++ b/chart.js\n@@\n-// draws the chart\n" + "+const rows = window.ANTON_DATA_prices;\n", + ) + assert written == {"chart.js"} + assert "ANTON_DATA_prices" in workspace.read("chart.js") diff --git a/tests/test_yolo_isolation.py b/tests/test_yolo_isolation.py new file mode 100644 index 00000000..1515f202 --- /dev/null +++ b/tests/test_yolo_isolation.py @@ -0,0 +1,91 @@ +"""Pin what the yolo engine is allowed to depend on. + +`agent.py` imports anton's `LLMClient` directly — a yolo run should get +the configured provider, the coding-model split, forced `tool_choice`, +pydantic validation and turn tracing, and the handler should pass the +same client the rest of the agent already holds. There is nothing to +abstract there. + +`patch.py` and `workspace.py` are different. They are pure functions over +strings and paths, and that is exactly why the engine is provable in +milliseconds instead of needing a provider. That property is easy to lose +to one convenience import, so it is pinned here. +""" + +from __future__ import annotations + +import ast +import pathlib + +# Pure: these must reach for nothing at all. +ENGINE = ["patch.py", "workspace.py"] +# Builds on the engine, and on nothing else in anton. +ENGINE_ADJACENT = ["data.py"] +YOLO = pathlib.Path(__file__).resolve().parents[1] / "anton" / "core" / "yolo" + + +def imports_of(filename: str) -> set[str]: + tree = ast.parse((YOLO / filename).read_text()) + found: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + found.update(alias.name.split(".")[0] for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module: + found.add(node.module.split(".")[0]) + return found + + +def test_the_engine_imports_only_the_standard_library(): + """No anton, no pydantic, no SDKs. If this fails, ask whether the + import is worth giving up a test suite that runs without a provider.""" + allowed = { + "__future__", "contextlib", "dataclasses", "datetime", "json", + "pathlib", "re", "signal", "threading", "typing", + } + for filename in ENGINE: + assert imports_of(filename) <= allowed, ( + f"{filename} imports {sorted(imports_of(filename) - allowed)}; " + "the engine is meant to be pure" + ) + + +def test_the_engine_adjacent_modules_reach_no_further_than_the_engine(): + """data.py may build on workspace.py. It may not reach into anton's + LLM layer, its settings, or its session — that would make the data + format untestable without a running agent.""" + import ast + + for filename in ENGINE_ADJACENT: + tree = ast.parse((YOLO / filename).read_text()) + for node in ast.walk(tree): + module = getattr(node, "module", None) + if isinstance(node, ast.ImportFrom) and module and module.startswith("anton"): + assert module.startswith("anton.core.yolo"), ( + f"{filename} imports {module}; engine-adjacent code may only " + "build on the yolo package itself" + ) + + +def test_the_engine_runs_with_no_provider_configured(tmp_path): + """The point of the rule above, demonstrated rather than asserted.""" + from anton.core.yolo.patch import parse_patch + from anton.core.yolo.workspace import Workspace + + workspace = Workspace(tmp_path) + workspace.write("f.txt", "before\n") + [file_patch] = parse_patch("--- a/f.txt\n+++ b/f.txt\n@@\n-before\n+after\n") + from anton.core.yolo.patch import apply_hunks + + assert apply_hunks(workspace.read("f.txt"), file_patch.hunks) == "after\n" + + +def test_the_loop_takes_the_real_client(): + """agent.py names LLMClient, so a wrong object is a type error at the + call site rather than a surprise at runtime.""" + import inspect + + from anton.core.llm.client import LLMClient + from anton.core.yolo import YoloEditor + + annotation = inspect.get_annotations(YoloEditor)["llm_client"] + assert annotation is LLMClient or annotation == "LLMClient" diff --git a/tests/test_yolo_patch.py b/tests/test_yolo_patch.py new file mode 100644 index 00000000..df47af0a --- /dev/null +++ b/tests/test_yolo_patch.py @@ -0,0 +1,199 @@ +"""Coverage for the deterministic patch engine. + +These are pure-string tests: no filesystem, no LLM, no anton. That is the +point of the module they cover — the valuable part of yolo mode is a set +of functions over strings, and it should be provable in milliseconds. + +The cases are the real failures seen while building yolocoder, not +invented ones. Where a test looks oddly specific, it is because a model +did exactly that. +""" + +from __future__ import annotations + +import pytest + +from anton.core.yolo.patch import ( + PatchError, + apply_hunks, + is_apply_patch_format, + locate, + parse_patch, +) + +# ─── The headline behaviour: numbers are ignored ──────────────────────── + + +def test_wrong_line_numbers_do_not_matter(): + """The whole reason this module exists. + + Every number in this header is a lie — wrong start, wrong counts. The + content is right, so it applies. + """ + content = "one\ntwo\nthree\nfour\n" + patch = ( + "--- a/f.txt\n" + "+++ b/f.txt\n" + "@@ -99,44 +1201,7 @@\n" + " two\n" + "-three\n" + "+THREE\n" + " four\n" + ) + [file_patch] = parse_patch(patch) + assert apply_hunks(content, file_patch.hunks) == "one\ntwo\nTHREE\nfour\n" + + +def test_a_hunk_with_no_trailing_context_still_applies(): + """git rejects this outright: a hunk ending on its last change asserts + the file ends there. Models write it constantly.""" + content = "header\ntitle: old\nfooter\n" + patch = "--- a/f\n+++ b/f\n@@\n header\n-title: old\n+title: new\n" + [file_patch] = parse_patch(patch) + assert apply_hunks(content, file_patch.hunks) == "header\ntitle: new\nfooter\n" + + +def test_indentation_drift_is_tolerated(): + """A model reproducing a file by eye gets the characters right and the + leading whitespace slightly wrong. The exact pass fails, the stripped + pass finds it.""" + content = "def f():\n return 1\n" + patch = "--- a/f\n+++ b/f\n@@\n- return 1\n+ return 2\n" + [file_patch] = parse_patch(patch) + assert "return 2" in apply_hunks(content, file_patch.hunks) + + +# ─── Refusing rather than guessing ────────────────────────────────────── + + +def test_an_unfindable_hunk_is_an_error_not_a_no_op(): + content = "alpha\nbeta\n" + patch = "--- a/f\n+++ b/f\n@@\n-gamma\n+delta\n" + [file_patch] = parse_patch(patch) + with pytest.raises(PatchError, match="could not find"): + apply_hunks(content, file_patch.hunks) + + +def test_a_short_ambiguous_hunk_is_refused(): + """Picking the first match is how an edit lands in the wrong function + and reports success.""" + lines = ["}", "", "}", "", "}"] + with pytest.raises(PatchError, match="ambiguous"): + locate(lines, ["}"]) + + +def test_a_long_repeated_block_is_placed_at_the_first_match(): + """Ambiguity only applies to blocks short enough to be a coincidence. + Three identical lines in a row is a real location.""" + lines = ["a", "b", "c", "x", "a", "b", "c"] + assert locate(lines, ["a", "b", "c"]) == 0 + + +def test_a_hunk_with_no_context_cannot_be_placed(): + content = "some\nexisting\ncontent\n" + with pytest.raises(PatchError, match="no context"): + apply_hunks(content, parse_patch("--- a/f\n+++ b/f\n@@\n+new\n")[0].hunks) + + +def test_a_pure_insertion_into_an_empty_file_is_unambiguous(): + """There is only one place it can go.""" + [file_patch] = parse_patch("--- a/f\n+++ b/f\n@@\n+hello\n+world\n") + assert apply_hunks("", file_patch.hunks) == "hello\nworld" + + +# ─── apply_patch (V4A) dialect ────────────────────────────────────────── + + +def test_add_file_creates_a_file_with_no_at_header(): + """'*** Add File:' is followed straight by its + lines. Waiting for an + '@@' reads the whole file as noise and creates nothing.""" + patch = ( + "*** Begin Patch\n" + "*** Add File: greet.py\n" + "+def hi():\n" + '+ return "hi"\n' + "*** End Patch\n" + ) + [file_patch] = parse_patch(patch) + assert file_patch.path == "greet.py" + assert apply_hunks("", file_patch.hunks) == 'def hi():\n return "hi"' + + +def test_update_file_drops_the_empty_hunk_its_header_creates(): + patch = ( + "*** Begin Patch\n" + "*** Update File: f.txt\n" + "@@\n" + " keep\n" + "-old\n" + "+new\n" + "*** End Patch\n" + ) + [file_patch] = parse_patch(patch) + assert len(file_patch.hunks) == 1 + assert apply_hunks("keep\nold\n", file_patch.hunks) == "keep\nnew\n" + + +def test_the_format_is_detected_without_being_confused_by_a_unified_diff(): + assert is_apply_patch_format("*** Begin Patch\n*** Add File: a\n+x\n") + assert not is_apply_patch_format("--- a/f\n+++ b/f\n@@\n-x\n+y\n") + assert not is_apply_patch_format("diff --git a/f b/f\n--- a/f\n") + + +def test_deleting_a_file_is_refused_loudly(): + with pytest.raises(PatchError, match="deletes"): + parse_patch("*** Begin Patch\n*** Delete File: important.txt\n*** End Patch\n") + + +# ─── Parsing edge cases that cost real debugging time ─────────────────── + + +def test_a_trailing_blank_line_is_not_context(): + """It comes from the patch ending in a newline. Counted as an empty + context line it makes every final hunk unmatchable.""" + [file_patch] = parse_patch("--- a/f\n+++ b/f\n@@\n keep\n-old\n+new\n\n\n") + assert file_patch.hunks[0].before == ["keep", "old"] + + +def test_an_empty_line_inside_a_hunk_is_context(): + content = "top\n\nbottom\n" + [file_patch] = parse_patch("--- a/f\n+++ b/f\n@@\n top\n\n-bottom\n+BOTTOM\n") + assert apply_hunks(content, file_patch.hunks) == "top\n\nBOTTOM\n" + + +def test_dev_null_headers_do_not_become_a_file_named_dev_null(): + patch = "--- /dev/null\n+++ b/new.txt\n@@\n+created\n" + [file_patch] = parse_patch(patch) + assert file_patch.path == "new.txt" + + +def test_ab_prefixes_are_stripped(): + [file_patch] = parse_patch("--- a/src/x.py\n+++ b/src/x.py\n@@\n-a\n+b\n") + assert file_patch.path == "src/x.py" + + +def test_a_patch_with_no_file_header_is_an_error(): + with pytest.raises(PatchError, match="no file headers"): + parse_patch("just some prose the model wrote\n") + + +def test_a_hunk_before_any_file_header_is_an_error(): + with pytest.raises(PatchError, match="before any file header"): + parse_patch("@@\n-a\n+b\n") + + +def test_several_hunks_in_one_file_compose(): + content = "one\ntwo\nthree\nfour\nfive\n" + patch = ( + "--- a/f\n+++ b/f\n" + "@@\n one\n-two\n+TWO\n" + "@@\n four\n-five\n+FIVE\n" + ) + [file_patch] = parse_patch(patch) + assert apply_hunks(content, file_patch.hunks) == "one\nTWO\nthree\nfour\nFIVE\n" + + +def test_a_no_newline_marker_is_ignored(): + patch = "--- a/f\n+++ b/f\n@@\n-old\n\\ No newline at end of file\n+new\n" + [file_patch] = parse_patch(patch) + assert apply_hunks("old", file_patch.hunks) == "new" diff --git a/tests/test_yolo_search.py b/tests/test_yolo_search.py new file mode 100644 index 00000000..0f66bc09 --- /dev/null +++ b/tests/test_yolo_search.py @@ -0,0 +1,223 @@ +"""Finding a file when its name does not tell you anything. + +The map lists paths. For a five-file artifact that is enough — the model +reads the names and knows where the title lives. For forty files it is +not, and no amount of retrying helps, because a retry can only widen to +files the model already guessed at. + +Search closes that. It is deterministic, calls no model, and its results +feed straight into the read set. +""" + +from __future__ import annotations + +import time +from pathlib import Path + +import pytest + +from anton.core.yolo import Change, ReadRequest, Workspace, YoloEditor +from anton.core.yolo.workspace import ( + MAX_MATCHES_PER_FILE, + MAX_MATCHES_PER_QUERY, + MAX_QUERIES, +) + +from tests.test_yolo_agent import StubCoder + + +def haystack(tmp_path: Path) -> Workspace: + """A folder where the names give nothing away.""" + workspace = Workspace(tmp_path) + workspace.write("a.js", "export const NAV = 1;\n") + workspace.write("b.js", "// nothing interesting\n") + workspace.write("c.js", "document.title = 'Old Title';\n") + workspace.write("d.js", "// also nothing\n") + return workspace + + +# ─── The search itself ────────────────────────────────────────────────── + + +def test_search_finds_the_file_the_name_did_not_reveal(tmp_path: Path): + [match] = haystack(tmp_path).search("Old Title").matches + assert match.path == "c.js" + assert match.line == 1 + assert "document.title" in match.text + + +def test_search_ignores_case(tmp_path: Path): + assert haystack(tmp_path).search("OLD TITLE").matches + assert haystack(tmp_path).search("old title").matches + + +def test_search_skips_generated_data(tmp_path: Path): + """Searching two megabytes of rows for a word returns thousands of + lines of noise and buries the one line of code that reads them.""" + workspace = haystack(tmp_path) + workspace.write("prices.data.js", "window.X=['Old Title','Old Title'];\n") + assert [m.path for m in workspace.search("Old Title").matches] == ["c.js"] + + +def test_one_common_word_cannot_flood_the_prompt(tmp_path: Path): + workspace = Workspace(tmp_path) + for name in "abcdefghij": + workspace.write(f"{name}.js", "hit\n" * 50) + matches = workspace.search("hit").matches + assert len(matches) <= MAX_MATCHES_PER_QUERY + per_file = [m for m in matches if m.path == "a.js"] + assert len(per_file) <= MAX_MATCHES_PER_FILE + + +def test_a_very_long_line_is_clipped(tmp_path: Path): + workspace = Workspace(tmp_path) + workspace.write("min.js", "x" * 5000 + "needle" + "y" * 5000) + [match] = workspace.search("needle").matches + assert len(match.text) < 400 + + +def test_no_matches_is_reported_not_omitted(tmp_path: Path): + """'That string is nowhere in this folder' is a real answer, and the + one that stops the model looking for it.""" + rendered, hits = haystack(tmp_path).search_many(["Old Title", "not here at all"]) + assert "c.js:1" in rendered + assert '"not here at all" — no matches' in rendered + assert hits == ["c.js"] + + +def test_an_empty_query_matches_nothing(tmp_path: Path): + assert haystack(tmp_path).search(" ").matches == [] + + +# ─── Search inside the loop ───────────────────────────────────────────── + + +async def test_a_search_at_the_pick_step_finds_the_file_to_edit(tmp_path: Path): + """The model cannot tell from a.js/b.js/c.js/d.js which sets the + title, so it searches instead of guessing. One LLM call still.""" + workspace = haystack(tmp_path) + coder = StubCoder( + ReadRequest(paths=[], search=["Old Title"]), + Change( + summary="Retitled", + files=["c.js"], + diff="--- a/c.js\n+++ b/c.js\n@@\n" + "-document.title = 'Old Title';\n+document.title = 'New Title';\n", + ), + ) + outcome = await YoloEditor(workspace=workspace, llm_client=coder).edit("retitle it") + + assert outcome.applied + assert len(coder.calls) == 2, "search must not cost an extra model call" + # The change request was shown both the hit and the file it is in. + request = coder.calls[1]["content"] + assert "SEARCH RESULTS:" in request + assert "c.js:1" in request + assert "document.title" in request + assert "New Title" in workspace.read("c.js") + + +async def test_the_model_can_search_to_recover_from_a_wrong_pick(tmp_path: Path): + """The recovery gap search was meant to close: it read the wrong file + and does not know the name of the right one.""" + workspace = haystack(tmp_path) + coder = StubCoder( + ReadRequest(paths=["a.js"]), # wrong file + Change(summary="", files=[], diff="", need_search=["Old Title"]), + Change( + summary="Retitled", + files=["c.js"], + diff="--- a/c.js\n+++ b/c.js\n@@\n" + "-document.title = 'Old Title';\n+document.title = 'New Title';\n", + ), + ) + outcome = await YoloEditor(workspace=workspace, llm_client=coder).edit("retitle it") + + assert outcome.applied + assert outcome.attempts == 2 + retry = coder.calls[2]["content"] + assert "c.js:1" in retry + assert "document.title = 'Old Title';" in retry # the file itself, now read + + +async def test_a_fruitless_search_still_terminates(tmp_path: Path): + """Searching for something that is not there must not loop.""" + workspace = haystack(tmp_path) + asking = Change(summary="", files=[], diff="", need_search=["not here at all"]) + coder = StubCoder(ReadRequest(paths=[]), asking, asking, asking) + outcome = await YoloEditor(workspace=workspace, llm_client=coder).edit("x") + + assert not outcome.applied + assert len(coder.calls) <= 4 + # And it was told the search found nothing, rather than left guessing. + assert "no matches" in coder.calls[2]["content"] + + +# ─── Regex, and the one hazard that comes with it ─────────────────────── + + +def test_search_takes_a_regular_expression(tmp_path: Path): + """The reason regex is worth the trouble: finding where a thing is + defined, not just where its exact spelling appears.""" + workspace = Workspace(tmp_path) + workspace.write("a.js", "function drawChart(rows) {}\n") + workspace.write("b.js", "function drawLegend(rows) {}\n") + workspace.write("c.js", "const notAFunction = 1;\n") + + found = workspace.search(r"function\s+draw\w+") + assert sorted(m.path for m in found.matches) == ["a.js", "b.js"] + assert not found.note + + +def test_a_plain_word_is_still_a_valid_pattern(tmp_path: Path): + assert haystack(tmp_path).search("Old Title").matches + + +def test_an_invalid_pattern_comes_back_as_feedback_not_a_crash(tmp_path: Path): + """A bad pattern is something the model can fix next attempt, so it is + handed back rather than raised.""" + found = haystack(tmp_path).search("unclosed (group") + assert found.matches == [] + assert "invalid pattern" in found.note + + +def test_a_runaway_pattern_is_cut_off_rather_than_hung(tmp_path: Path): + """The hazard that made this worth thinking about. `(a+)+b` against + thirty characters takes ~46 seconds and grows exponentially, so no cap + on line or file size contains it — only a wall-clock interrupt does.""" + workspace = Workspace(tmp_path) + workspace.write("evil.txt", "a" * 40 + "!") + + started = time.monotonic() + found = workspace.search(r"(a+)+b") + elapsed = time.monotonic() - started + + assert elapsed < 5, f"took {elapsed:.1f}s — the interrupt did not fire" + assert "timed out" in found.note + # And it says what to do about it. + assert "nested quantifiers" in found.note + + +async def test_a_runaway_pattern_does_not_take_the_run_down(tmp_path: Path): + """It has to degrade to 'no matches', not an exception.""" + workspace = haystack(tmp_path) + workspace.write("evil.txt", "a" * 40 + "!") + coder = StubCoder( + ReadRequest(paths=[], search=[r"(a+)+b"]), + Change( + summary="Retitled", + files=["c.js"], + diff="--- a/c.js\n+++ b/c.js\n@@\n" + "-document.title = 'Old Title';\n+document.title = 'New';\n", + ), + ) + outcome = await YoloEditor(workspace=workspace, llm_client=coder).edit("retitle") + assert outcome.applied + assert "timed out" in coder.calls[1]["content"] + + +def test_too_many_queries_are_dropped_and_said_so(tmp_path: Path): + """The worst case is timeout x queries, so the count is capped too.""" + rendered, _ = haystack(tmp_path).search_many([f"q{n}" for n in range(12)]) + assert "not run" in rendered + assert f"at most {MAX_QUERIES}" in rendered diff --git a/uv.lock b/uv.lock index 54c3b5ec..2cfe4f18 100644 --- a/uv.lock +++ b/uv.lock @@ -1,129 +1,9 @@ version = 1 revision = 3 requires-python = ">=3.11" - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, - { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, - { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, - { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, -] - -[[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" }, +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' or sys_platform != 'emscripten'", ] [[package]] @@ -146,31 +26,29 @@ wheels = [ [[package]] name = "anthropic" -version = "0.83.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "distro" }, { name = "docstring-parser" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/e5/02cd2919ec327b24234abb73082e6ab84c451182cc3cc60681af700f4c63/anthropic-0.83.0.tar.gz", hash = "sha256:a8732c68b41869266c3034541a31a29d8be0f8cd0a714f9edce3128b351eceb4", size = 534058, upload-time = "2026-02-19T19:26:38.904Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/50/463166f02179ab279edb61de1589a6f69cb3838d6a2fb6f2c92a3f8042f1/anthropic-1.3.0.tar.gz", hash = "sha256:6873492a77ede8849a161ab1bc78bc9a1e492a006d0b5bb4c57ac77845df838a", size = 1148177, upload-time = "2026-09-01T17:37:10.392Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/75/b9d58e4e2a4b1fc3e75ffbab978f999baf8b7c4ba9f96e60edb918ba386b/anthropic-0.83.0-py3-none-any.whl", hash = "sha256:f069ef508c73b8f9152e8850830d92bd5ef185645dbacf234bb213344a274810", size = 456991, upload-time = "2026-02-19T19:26:40.114Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5d/7863a9961d320c23787c7b594956afe4e878f9c0ae2376b11a20e416791d/anthropic-1.3.0-py3-none-any.whl", hash = "sha256:e7e7dbebf9f3c84a23954ab989378af6ae10a4d1804c81e9fea4b5ced695ce75", size = 1296959, upload-time = "2026-09-01T17:37:08.525Z" }, ] [[package]] name = "anton-agent" source = { editable = "." } dependencies = [ - { name = "aiohttp" }, { name = "anthropic" }, { name = "dill" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "openai" }, { name = "packaging" }, { name = "prompt-toolkit" }, @@ -195,11 +73,10 @@ dev = [ [package.metadata] requires-dist = [ - { name = "aiohttp", specifier = ">=3.9" }, - { name = "anthropic", specifier = ">=0.42.0" }, + { name = "anthropic", specifier = ">=1.0" }, { name = "dill", specifier = "==0.3.8" }, - { name = "httpx", specifier = ">=0.27,<1" }, - { name = "openai", specifier = ">=2.21.0" }, + { name = "httpx2", specifier = ">=2.7,<3" }, + { name = "openai", specifier = ">=3.0" }, { name = "packaging", specifier = ">=21.0" }, { name = "pillow", marker = "extra == 'clipboard'", specifier = ">=12.3.0" }, { name = "prompt-toolkit", specifier = ">=3.0" }, @@ -231,24 +108,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[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 = "certifi" -version = "2026.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, -] - [[package]] name = "click" version = "8.3.1" @@ -279,15 +138,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252, upload-time = "2024-01-27T23:42:14.239Z" }, ] -[[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.17.0" @@ -297,111 +147,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] -[[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/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 = "h11" version = "0.16.0" @@ -412,40 +157,51 @@ wheels = [ ] [[package]] -name = "httpcore" -version = "1.0.9" +name = "httpcore2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "h11" }, + { name = "h11", marker = "python_full_version < '3.12' or sys_platform != 'emscripten'" }, + { name = "truststore", marker = "python_full_version < '3.12' or sys_platform != 'emscripten'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, ] [[package]] -name = "httpx" -version = "0.28.1" +name = "httpx2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, ] -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" } + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] name = "idna" -version = "3.11" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +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/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { 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]] @@ -459,87 +215,88 @@ wheels = [ [[package]] name = "jiter" -version = "0.13.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +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/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]] @@ -563,140 +320,21 @@ 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 = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -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/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 = "openai" -version = "2.21.0" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, - { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/e5/3d197a0947a166649f566706d7a4c8f7fe38f1fa7b24c9bcffe4c7591d44/openai-2.21.0.tar.gz", hash = "sha256:81b48ce4b8bbb2cc3af02047ceb19561f7b1dc0d4e52d1de7f02abfd15aa59b7", size = 644374, upload-time = "2026-02-14T00:12:01.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/76/913b755a1a6b54e2d9140eb8d488aa0d47c7359b1d7eac5e864cb7913bbf/openai-3.6.0.tar.gz", hash = "sha256:18fe3f6e96390ef41ee27b152fc9effefca321c33673bd9b956a572493d3ab9b", size = 1455376, upload-time = "2026-08-28T22:29:18.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/56/0a89092a453bb2c676d66abee44f863e742b2110d4dbb1dbcca3f7e5fc33/openai-2.21.0-py3-none-any.whl", hash = "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", size = 1103065, upload-time = "2026-02-14T00:11:59.603Z" }, + { url = "https://files.pythonhosted.org/packages/a1/94/805b87ecc951c49ec8f247f5e8eb324ab064bd2ad73b6a0e704dd49aa073/openai-3.6.0-py3-none-any.whl", hash = "sha256:508e2158bf971687f953b62e44b02f207792c815aac306816386d7ba34d37f5f", size = 1699841, upload-time = "2026-08-28T22:29:16.436Z" }, ] [[package]] @@ -814,105 +452,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] -[[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, -] - [[package]] name = "pydantic" version = "2.12.5" @@ -1172,18 +711,6 @@ 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 = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - [[package]] name = "truststore" version = "0.10.4" @@ -1237,125 +764,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a3 wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] - -[[package]] -name = "yarl" -version = "1.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, - { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, - { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, - { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, - { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, - { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, - { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, - { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, - { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, - { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, -]