diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4d25338..73f67cb 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -17,6 +17,7 @@ jobs:
enable-cache: true
- run: uv sync --dev # no anthropic extra: keeps CI keyless, matches design
- run: uv run ruff check .
+ - run: uv run mypy # checked annotations (audit fix #1); config in pyproject
pytest:
runs-on: ubuntu-latest
strategy:
diff --git a/.gitignore b/.gitignore
index 5f87094..a7eb885 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ reports/
# Ad-hoc experiment harnesses (e.g. the fidelity probe) — not part of the lib
scratch_fidelity/
.codegraph
+.coverage
diff --git a/.runechoguardignore b/.runechoguardignore
index 3292e3c..19ef642 100644
--- a/.runechoguardignore
+++ b/.runechoguardignore
@@ -81,3 +81,6 @@ DEFAULT_PROBE_REPORT
# probes.py cross_call_overlap docstring: "When `idf` is supplied (from `token_idf`)" —
# "supplied (from" reads as a bare call to the guard's pattern match. Not code.
supplied
+# proxy.py _emit_audit: `audit = self.audit` narrowed local (mypy baseline, audit
+# fix #1), called bare — same local-variable false-positive class as real/orig_send.
+audit
diff --git a/USAGE.md b/USAGE.md
index 322756d..d70057a 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -137,9 +137,11 @@ install invocation. It honors `$CLAUDE_CONFIG` if your config isn't at
`~/.claude.json`. Start with one high-win, read-only server (e.g. `runecho`) and
confirm it works before wrapping more.
-One sharp edge worth knowing: a re-wrap rebuilds the entry from the **stashed
-original**, so any hand-edit you made to the *wrapped* entry (say, an `env.PATH`
-pin) is dropped — apply such edits to the stash original too.
+Hand-edits to a wrapped entry (say, an `env.PATH` pin) survive re-installs: the
+re-wrap rebuilds `command`/`args` from the stashed original but keeps every other
+live key, and reports what it carried (`kept hand-edited key(s) …`). One edge
+remains: `uninstall-mcp` restores the **pre-terse original**, which does not carry
+such edits — put them on the original entry too if they must survive an uninstall.
Claude Code has three MCP scopes, and `--scope` targets any of them (default
`user`, i.e. today's behavior):
diff --git a/pyproject.toml b/pyproject.toml
index fd5bd3d..bbaca35 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,7 +14,7 @@ dependencies = [
terse = "terse.cli:main"
[dependency-groups]
-dev = ["pytest", "ruff"]
+dev = ["pytest", "ruff", "mypy"]
[build-system]
requires = ["hatchling"]
@@ -22,3 +22,27 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/terse"]
+
+[tool.ruff.lint]
+# Curated beyond ruff's minimal default (E4/E7/E9/F): import order, real bug
+# patterns, modern-syntax upgrades, comprehension and simplification wins. Style-only
+# packs are deliberately left out — the gate is for defects, not formatting taste.
+select = ["E4", "E7", "E9", "F", "I", "B", "UP", "C4", "SIM", "RET", "PLE"]
+ignore = [
+ "SIM108", # ternary-vs-if is a readability call, not a defect
+ "SIM105", # contextlib.suppress: try/except-pass is clearer at fail-open sites
+]
+
+[tool.mypy]
+# Lenient baseline (audit fix #1): the goal is to make the existing annotations a
+# CHECKED contract, not to win strictness awards on day one. Ratchet later.
+python_version = "3.11"
+files = ["src/terse"]
+warn_unused_ignores = true
+warn_redundant_casts = true
+no_implicit_optional = true
+check_untyped_defs = true
+
+[[tool.mypy.overrides]]
+module = "tiktoken"
+ignore_missing_imports = true
diff --git a/src/terse/capture.py b/src/terse/capture.py
index 60707b4..6444378 100644
--- a/src/terse/capture.py
+++ b/src/terse/capture.py
@@ -21,8 +21,9 @@
from typing import Any
from ._secure_io import append_restricted, write_restricted
-
-from .transforms import _uniform_dict_list # the one canonical "what tabularize folds" rule
+from .transforms import (
+ _uniform_dict_list, # the one canonical "what tabularize folds" rule
+)
# Shape buckets. classify_shape returns one of these.
PRETTY_JSON = "pretty-json"
diff --git a/src/terse/cli.py b/src/terse/cli.py
index e98408c..f708348 100644
--- a/src/terse/cli.py
+++ b/src/terse/cli.py
@@ -15,15 +15,22 @@
import argparse
import json
+import json as _json
import re
import sys
+from datetime import UTC
from pathlib import Path
-import json as _json
-
from . import transforms
from ._secure_io import write_restricted
-from .capture import capture_payload, classify_shape, coverage, extract_records, load_corpus
+from .capture import (
+ capture_payload,
+ classify_shape,
+ coverage,
+ extract_records,
+ load_corpus,
+)
+from .html_report import build_html_report
from .measure import cross_tokenizer_savings, measure_corpus
from .probes import (
cross_call_overlap,
@@ -32,7 +39,6 @@
server_of_tool,
value_redundancy,
)
-from .html_report import build_html_report
from .report import (
build_cross_server_probe_report,
build_probe_report,
@@ -81,14 +87,14 @@ def _record_and_print_trend(history_path: str, rows: list, label: str) -> None:
in this command that reads the real clock (principle #31: inject nondeterminism
at the edge, not inside the pure summarize_run/build_trend_report/
trend_sparkline_lines core)."""
- from datetime import datetime, timezone
+ from datetime import datetime
from .history import append_run, load_history, summarize_run
from .report import build_trend_report
from .terminal_report import trend_sparkline_lines
path = Path(history_path)
- ts = datetime.now(timezone.utc).isoformat(timespec="seconds")
+ ts = datetime.now(UTC).isoformat(timespec="seconds")
run = summarize_run(rows, ts, label=label)
all_runs = load_history(path) + [run]
append_run(path, run)
@@ -302,7 +308,7 @@ def _cmd_probe(args: argparse.Namespace) -> int:
by_tool.setdefault(env["tool"], []).append(env)
for tool, envs in by_tool.items():
envs = sorted(envs, key=lambda e: e.get("sha", ""))
- for prev, curr in zip(envs, envs[1:]):
+ for prev, curr in zip(envs, envs[1:], strict=False): # sliding pairs
res = cross_call_overlap(prev["raw"], curr["raw"])
if res.get("available"):
overlap_rows.append({"tool": tool, "prev_sha": prev.get("sha", "?"),
@@ -604,6 +610,10 @@ def _cmd_install_mcp(args: argparse.Namespace) -> int:
print(f"{tag} {c['server']}:")
print(f" before: {_short_cmd(c['before'])}")
print(f" after: {_short_cmd(c['after'])}")
+ if c.get("preserved"):
+ print(f" kept hand-edited key(s) from the live entry: "
+ f"{', '.join(c['preserved'])} (note: uninstall restores the "
+ f"pre-terse original, which does NOT carry them)")
print(f"config: {res['config']} scope: {res['scope']} policy: {res['policy']}")
if res.get("capture_dir"):
print(f"capture: raw tool results → {res['capture_dir']}")
diff --git a/src/terse/dropeval.py b/src/terse/dropeval.py
index e5614e9..a8c428a 100644
--- a/src/terse/dropeval.py
+++ b/src/terse/dropeval.py
@@ -26,15 +26,16 @@
import json
import urllib.request
+from collections.abc import Callable
from dataclasses import dataclass, field
-from typing import Any, Callable, Optional
+from typing import Any
-from . import capture
-from . import fluency
+from . import capture, fluency
from . import lossy as lossy_mod
from . import policy as policy_mod
from .proxy import TERSE_PRIMER
+
# --------------------------------------------------------------------------- #
# Tool-loop answerer protocol — the existing fluency.Answerer (system, user) -> str
# can't express a tool call; this is a provider-neutral running conversation instead.
@@ -94,7 +95,7 @@ def _staged_apply(obj: Any, rule: Any, tool: str) -> tuple[policy_mod.Applied, d
def _questions_and_staging(
obj: Any, rule: Any, tool: str
-) -> tuple[list[DropQuestion], Optional[policy_mod.Applied], Optional[dict[str, Any]]]:
+) -> tuple[list[DropQuestion], policy_mod.Applied | None, dict[str, Any] | None]:
"""Shared core of `gen_drop_questions`: generates the (recall, precision) question
pair AND returns the `(applied, staging)` that `_staged_apply` computed along the
way, so `run_drop_payload` can reuse them instead of a second `policy.apply()` pass
@@ -190,7 +191,7 @@ def gen_drop_questions(obj: Any, rule: Any, tool: str) -> list[DropQuestion]:
# --------------------------------------------------------------------------- #
# The 2-turn tool-loop driver — mirrors the real proxy's retrieve protocol exactly
# --------------------------------------------------------------------------- #
-def _miss_text(handle: str) -> str:
+def _miss_text(handle: Any) -> str:
"""The exact miss string `proxy.Interceptor.answer_retrieve` emits for an unresolved
handle, copied verbatim so this eval's miss-handling matches production behavior — a
model that has learned to recover from a real miss must see the same words here."""
diff --git a/src/terse/fluency.py b/src/terse/fluency.py
index 048650b..1cff295 100644
--- a/src/terse/fluency.py
+++ b/src/terse/fluency.py
@@ -34,15 +34,23 @@
import json
import re
import urllib.request
-from urllib.parse import urlsplit
+from collections.abc import Callable
from dataclasses import dataclass
-from typing import Any, Callable
+from typing import Any
+from urllib.parse import urlsplit
from . import text_diff
from .capture import LONG_TEXT, OTHER, classify_shape, extract_records
from .tokenize import count_cl100k
-from .transforms import (compress, compress_structure, dict_encode, diff_wire,
- has_terse_marker, minify, _uniform_dict_list)
+from .transforms import (
+ _uniform_dict_list,
+ compress,
+ compress_structure,
+ dict_encode,
+ diff_wire,
+ has_terse_marker,
+ minify,
+)
# Loopback hosts where cleartext http is safe (never leaves the machine), so a Bearer
# key over http to one of these is fine — a local LiteLLM/CCR gateway is a common setup.
@@ -187,7 +195,7 @@ def _records_of(lst: Any) -> list[str] | None:
# dict-map of parent records -> the first parent (map order) with a child record list
if isinstance(v, dict) and v and all(isinstance(pv, dict) for pv in v.values()):
for pkey, parent in v.items():
- for ck, cv in parent.items():
+ for _ck, cv in parent.items():
if _records_of(cv):
return f"{k}[{json.dumps(pkey, ensure_ascii=False)}]", cv, _intersection_cols(cv)
# a NON-uniform top-level list of dicts (uniform ones are extract_records' domain)
@@ -561,7 +569,7 @@ def _iter_consecutive_pairs(envelopes: list[dict]):
by_tool.setdefault(env["tool"], []).append(env)
for tool, envs in by_tool.items():
envs = sorted(envs, key=lambda e: e.get("sha", ""))
- for prev_env, curr_env in zip(envs, envs[1:]):
+ for prev_env, curr_env in zip(envs, envs[1:], strict=False): # sliding pairs
yield tool, curr_env.get("sha", "?"), prev_env["raw"], curr_env["raw"]
@@ -627,7 +635,7 @@ def build_chain_windows(envelopes: list[dict], max_depth: int = 5,
except (json.JSONDecodeError, TypeError):
seq.append((e.get("sha", "?"), None))
cur: list[tuple] = []
- for (psha, p), (csha, c) in zip(seq, seq[1:]):
+ for (psha, p), (csha, c) in zip(seq, seq[1:], strict=False): # sliding pairs
ok = (p is not None and c is not None
and diff_wire(p, c, tool) is not None)
if ok:
@@ -643,9 +651,9 @@ def build_chain_windows(envelopes: list[dict], max_depth: int = 5,
windows: list[tuple] = []
for depth in range(1, max_depth + 1):
# every eligible start per tool, spread over its runs; then round-robin
- candidates: dict[str, list[tuple]] = {}
+ candidates: dict[str, list[list[tuple]]] = {}
for tool, tool_runs in sorted(runs.items()):
- starts: list[tuple] = []
+ starts: list[list[tuple]] = []
for run in tool_runs:
for i in range(0, len(run) - depth, depth): # non-overlapping
window = run[i:i + depth + 1]
@@ -656,7 +664,7 @@ def build_chain_windows(envelopes: list[dict], max_depth: int = 5,
picked = 0
idx = 0
while picked < per_depth_cap and candidates:
- for tool in sorted(list(candidates)):
+ for tool in sorted(candidates):
starts = candidates[tool]
if idx >= len(starts):
del candidates[tool]
@@ -682,7 +690,7 @@ def run_chain_payload(objs: list, answerer: Answerer, tool: str = "",
if not questions:
return []
wires: list[str] = []
- for prev, curr in zip(objs, objs[1:]):
+ for prev, curr in zip(objs, objs[1:], strict=False): # sliding pairs
wire = diff_wire(prev, curr, tool)
if wire is None:
return []
diff --git a/src/terse/html_report.py b/src/terse/html_report.py
index 6fc18fa..9b21c5d 100644
--- a/src/terse/html_report.py
+++ b/src/terse/html_report.py
@@ -17,6 +17,7 @@
from __future__ import annotations
import html as _html
+from collections.abc import Sequence
from typing import Any
from .report import _ci, _form_stats, _pct, _sum, _worst_case_gap
@@ -147,7 +148,7 @@ def diverging_bar_chart(items: list[tuple[str, float]], unit: str = "%") -> str:
return "".join(out)
-def stacked_bar_chart(items: list[tuple[str, list[float]]],
+def stacked_bar_chart(items: Sequence[tuple[str, Sequence[float]]],
series_labels: tuple[str, ...],
series_colors: tuple[str, ...] = ("var(--series-1)", "var(--series-2)", "var(--series-3)")
) -> str:
@@ -187,7 +188,7 @@ def sx(v: float) -> float:
zero_x = sx(0.0)
out = [_svg_open(width, height)]
lx, ly = label_w, 14
- for name, color in zip(series_labels, series_colors):
+ for name, color in zip(series_labels, series_colors, strict=True):
out.append(f'')
out.append(f'{_esc(name)}')
lx += 18 + 9 * len(name)
diff --git a/src/terse/install_mcp.py b/src/terse/install_mcp.py
index 9fd728c..14e7e74 100644
--- a/src/terse/install_mcp.py
+++ b/src/terse/install_mcp.py
@@ -149,7 +149,9 @@ def wrap(config: dict, stash: dict, server: str, policy: str,
"""Wrap `server`'s entry with the terse proxy. Idempotent: if already managed
(present in stash), re-wrap from the stashed original so policy/cmd updates
apply cleanly without nesting proxies. Preserves all non-command/args (and, for a
- URL entry, non-url/headers) keys (env, cwd, type, …) of the original entry. With
+ URL entry, non-url/headers) keys (env, cwd, type, …) of the original entry — and,
+ on a re-wrap, hand-edits made to those keys on the LIVE wrapped entry win over the
+ stashed original's values (the drift guard below). With
`capture_dir`, the wrapped proxy tees raw tool results into that corpus for later
measurement (#32). `diff` is tri-state: None writes no flag (the entry inherits
the proxy default — ON since #75), True/False bake `--diff`/`--no-diff` into the
@@ -161,10 +163,11 @@ def wrap(config: dict, stash: dict, server: str, policy: str,
forwarded as repeated `--header k=v` (see `transport.HttpTransport`). Anything with
neither key is not a valid MCP server entry and can't be wrapped."""
servers = config.setdefault("mcpServers", {})
+ live = servers.get(server)
if server in stash:
original = stash[server]
- elif server in servers:
- original = servers[server]
+ elif live is not None:
+ original = live
stash[server] = original
else:
raise KeyError(server)
@@ -199,6 +202,18 @@ def wrap(config: dict, stash: dict, server: str, policy: str,
if k not in ("command", "args", "url", "headers")}
new_entry["command"] = terse_cmd[0]
new_entry["args"] = [*terse_cmd[1:], "proxy", *proxy_opts, *header_opts, "--", orig_url]
+
+ if live is not None and live is not original:
+ # Drift guard: a re-wrap used to rebuild the entry purely from the stashed
+ # original, silently reverting any hand-edit made to the WRAPPED entry since
+ # the last install (a scoped env.PATH pin, a cwd) — that reverted codegraph's
+ # node pin in production on 2026-07-13. command/args are terse-owned and always
+ # rebuilt (flags must reflect this invocation); url/headers never appear on a
+ # wrapped entry (they're folded into args), so a drifted live copy of them must
+ # not be resurrected either. Everything else on the live entry wins.
+ for k, v in live.items():
+ if k not in ("command", "args", "url", "headers"):
+ new_entry[k] = v
servers[server] = new_entry
return config, stash
@@ -306,10 +321,19 @@ def do_install(servers: list[str], policy: str, *, dry_run: bool = False,
changes = []
for s in servers:
before = (node.get("mcpServers") or {}).get(s)
+ # Hand-edits = non-terse-owned keys on the live WRAPPED entry that differ from
+ # the stashed original — the drift guard in wrap() carries them forward; name
+ # them in the result so the operator sees what survived (and what to move into
+ # the original entry if it should also survive an uninstall).
+ preserved = sorted(
+ k for k in (before or {})
+ if k not in ("command", "args", "url", "headers")
+ and s in stash and (before or {}).get(k) != stash[s].get(k)
+ )
wrap(node, stash, s, policy_abs, terse_cmd, capture_dir=capture_abs,
diff=diff, diff_keyframe_interval=diff_keyframe_interval)
changes.append({"server": s, "before": before,
- "after": node["mcpServers"][s]})
+ "after": node["mcpServers"][s], "preserved": preserved})
result = {"config": str(target.cfg), "scope": scope, "policy": policy_abs,
"available": available, "changes": changes, "dry_run": dry_run,
diff --git a/src/terse/lossy.py b/src/terse/lossy.py
index 29bd63d..b9f25cd 100644
--- a/src/terse/lossy.py
+++ b/src/terse/lossy.py
@@ -25,7 +25,9 @@
import hashlib
import json
-from typing import Any, Callable, Iterator
+from collections.abc import Callable, Iterator
+from functools import partial
+from typing import Any
from .transforms import DROPPED_MARKER as DROP_KEY
@@ -103,7 +105,7 @@ def _copy_at(dst: Any, src: Any, tokens: list[str]) -> Any:
if head == "[]":
if not (isinstance(dst, list) and isinstance(src, list) and len(dst) == len(src)):
raise PathError("list length/shape mismatch at '[]'")
- return [_copy_at(d, s, rest) for d, s in zip(dst, src)]
+ return [_copy_at(d, s, rest) for d, s in zip(dst, src, strict=True)] # len-checked above
if not (isinstance(dst, dict) and isinstance(src, dict)):
raise PathError(f"object mismatch at {head!r}")
if head not in dst:
@@ -122,7 +124,7 @@ def _leaf_pairs(orig: Any, out: Any, tokens: list[str]) -> Iterator[tuple[Any, A
if head == "[]":
if not (isinstance(orig, list) and isinstance(out, list) and len(orig) == len(out)):
raise PathError("list length/shape mismatch at '[]'")
- for o, t in zip(orig, out):
+ for o, t in zip(orig, out, strict=True): # len-checked above
yield from _leaf_pairs(o, t, rest)
return
if not (isinstance(orig, dict) and isinstance(out, dict)):
@@ -149,7 +151,7 @@ def apply_lossy(obj: Any, rule: Any) -> Any:
out = obj
for path, spec in _truncate_specs(rule):
max_len = int(spec.get("max", DEFAULT_MAX))
- out = _apply_at(out, _parse_path(path), lambda v, m=max_len: _truncate(v, m))
+ out = _apply_at(out, _parse_path(path), partial(_truncate, max_len=max_len))
return out
@@ -188,7 +190,7 @@ def _handle(tool: str, path: str, serialized: str) -> str:
"""Content-addressed handle for a dropped value. Includes tool+path so identical bytes
under different fields get distinct handles (clearer provenance), and is stable across
runs (no RNG) so the same value dropped twice reuses one store slot."""
- digest = hashlib.sha1(f"{tool}\x00{path}\x00{serialized}".encode("utf-8")).hexdigest()
+ digest = hashlib.sha1(f"{tool}\x00{path}\x00{serialized}".encode()).hexdigest()
return digest[:HANDLE_LEN]
@@ -223,7 +225,7 @@ def apply_drops(obj: Any, rule: Any, tool: str, sink: Callable[[str, Any], None]
for path, spec in _drop_specs(rule):
min_len = int(spec.get("min", DEFAULT_DROP_MIN))
out = _apply_at(out, _parse_path(path),
- lambda v, _p=path, _m=min_len: _drop(v, tool, _p, _m, sink))
+ partial(_drop, tool=tool, path=path, min_len=min_len, sink=sink))
return out
diff --git a/src/terse/measure.py b/src/terse/measure.py
index 95ff606..b87bc04 100644
--- a/src/terse/measure.py
+++ b/src/terse/measure.py
@@ -19,7 +19,7 @@
from __future__ import annotations
import json
-from typing import Any, Optional
+from typing import Any
from . import transforms
from .capture import classify_shape
@@ -54,10 +54,10 @@ def measure_payload(raw: str) -> dict[str, Any]:
tab_tok = count_cl100k(tabular)
cmp_tok = count_cl100k(compressed)
- def _saved(a: Optional[int], b: Optional[int]) -> Optional[int]:
+ def _saved(a: int | None, b: int | None) -> int | None:
return None if a is None or b is None else a - b
- row = {
+ return {
"shape": shape,
"applicable": True,
"roundtrip_ok": gate,
@@ -69,7 +69,6 @@ def _saved(a: Optional[int], b: Optional[int]) -> Optional[int]:
"tier_total": _saved(raw_tok, cmp_tok),
},
}
- return row
def cross_tokenizer_savings(envelopes: list[dict[str, Any]]) -> list[dict[str, Any]]:
diff --git a/src/terse/multiproxy.py b/src/terse/multiproxy.py
index 2fb9724..0bf8b2e 100644
--- a/src/terse/multiproxy.py
+++ b/src/terse/multiproxy.py
@@ -56,10 +56,11 @@
import threading
import time
from collections import OrderedDict
+from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from threading import Lock, Thread
-from typing import Any, Callable, Optional, TextIO
+from typing import Any, TextIO
from . import lossy as lossy_mod
from . import policy as policy_mod
@@ -150,7 +151,7 @@ class DownstreamSpec:
name: str
target: list[str] # a stdio command, or a single-element [url]
headers: dict[str, str]
- policy_path: Optional[str] # resolved relative to the config file; None = use the default
+ policy_path: str | None # resolved relative to the config file; None = use the default
def load_multi_config(path: str) -> list[DownstreamSpec]:
@@ -244,7 +245,7 @@ class _PendingBroadcast:
client_id: Any
remaining: set[int]
parts: dict[int, dict] = field(default_factory=dict)
- timer: Optional[threading.Timer] = None
+ timer: threading.Timer | None = None
done: bool = False
@@ -268,7 +269,7 @@ class _PeerSender:
def __init__(self, transport: Transport, debug: bool = False):
self._transport = transport
self._debug = debug
- self._q: "queue.Queue[Any]" = queue.Queue()
+ self._q: queue.Queue[Any] = queue.Queue()
self._thread = Thread(target=self._run, daemon=True)
self._thread.start()
@@ -338,13 +339,13 @@ def __init__(self, peers: list[Peer], out: TextIO, out_lock: Lock, *,
# went out must still resolve here and be swallowed (see _maybe_collect), not
# fall through to a peer's Interceptor as an unsolicited message. Bounded by
# _LOCAL_ID_MAP_MAX eviction instead of eager per-broadcast cleanup.
- self._local_id_map: "OrderedDict[Any, int]" = OrderedDict()
+ self._local_id_map: OrderedDict[Any, int] = OrderedDict()
# router-local id -> (peer_idx, original id), for a server-initiated request
# forwarded to the client (see _rewrite_server_request) so the client's eventual
# reply can be routed back to the peer that actually asked, with its original id
# restored — instead of the prior v1 gap (misdelivered to peer 0). Bounded by
# _SERVER_REQ_MAX eviction.
- self._server_requests: "OrderedDict[str, tuple[int, Any]]" = OrderedDict()
+ self._server_requests: OrderedDict[str, tuple[int, Any]] = OrderedDict()
self._server_req_seq = 0
# client id -> Timer, for a routed (single-peer) tools/call awaiting reply —
# mirrors _broadcast's BROADCAST_TIMEOUT guarantee ("a dead peer can't wedge
@@ -357,7 +358,7 @@ def __init__(self, peers: list[Peer], out: TextIO, out_lock: Lock, *,
# monotonic time that happened — the peer's eventual real (late) reply must be
# swallowed here, not double-delivered. Aged out by `_routed_timed_out_ttl`, not
# by population count (see `_ROUTED_TIMED_OUT_MAX`'s comment above).
- self._routed_timed_out: "OrderedDict[Any, float]" = OrderedDict()
+ self._routed_timed_out: OrderedDict[Any, float] = OrderedDict()
self._routed_timed_out_ttl = broadcast_timeout * 4
# ---------- client -> server ----------
@@ -846,7 +847,7 @@ def _merge_initialize(self, pb: _PendingBroadcast) -> dict:
meaningful). `instructions` = ONE `TERSE_PRIMER`, first, then each peer's own
non-empty instructions (skipping one that already carries the primer, so a peer
that is ITSELF a terse proxy doesn't duplicate it)."""
- protocol_version: Optional[str] = None
+ protocol_version: str | None = None
capabilities: dict = {}
instructions_parts: list[str] = []
# Iterate pb.parts in ITS OWN (insertion) order, not range(len(self.peers))
@@ -929,11 +930,11 @@ def _scatter_first_success(self, pb: _PendingBroadcast) -> dict:
def _build_peers(specs: list[DownstreamSpec], default_policy: policy_mod.Policy, *,
- debug: bool, capture: Optional[Callable[[str, str], None]],
- audit: Optional[Callable[[dict], None]],
- store: "OrderedDict[str, Any]", store_lock: Lock,
- dropped_bytes: list[int], diff_override: Optional[bool] = None,
- diff_keyframe_override: Optional[int] = None) -> list[Peer]:
+ debug: bool, capture: Callable[[str, str], None] | None,
+ audit: Callable[[dict], None] | None,
+ store: OrderedDict[str, Any], store_lock: Lock,
+ dropped_bytes: list[int], diff_override: bool | None = None,
+ diff_keyframe_override: int | None = None) -> list[Peer]:
"""Build every `Peer`: its own `Transport` (stdio or HTTP, via `build_transport`)
and its own `Interceptor` (per-peer diff/compress state, but the drop store —
including its byte-eviction counter — is injected shared). Raises on a bad spec —
@@ -973,13 +974,13 @@ def run_multi_proxy(
default_policy: policy_mod.Policy,
*,
debug: bool = False,
- stdin: Optional[TextIO] = None,
- stdout: Optional[TextIO] = None,
- capture_dir: Optional[str] = None,
- debug_log: Optional[str] = None,
+ stdin: TextIO | None = None,
+ stdout: TextIO | None = None,
+ capture_dir: str | None = None,
+ debug_log: str | None = None,
broadcast_timeout: float = BROADCAST_TIMEOUT,
- diff_override: Optional[bool] = None,
- diff_keyframe_override: Optional[int] = None,
+ diff_override: bool | None = None,
+ diff_keyframe_override: int | None = None,
) -> int:
"""Load `config_path`, build one `Peer` per downstream (own `Transport` + own
`Interceptor`, all sharing one drop store), spawn one `pump()` reader thread per
@@ -1008,7 +1009,7 @@ def run_multi_proxy(
capture, audit = _build_capture_and_audit(capture_dir, debug_log, debug, "[terse-multiproxy]")
- store: "OrderedDict[str, Any]" = OrderedDict()
+ store: OrderedDict[str, Any] = OrderedDict()
store_lock = Lock()
dropped_bytes: list[int] = [0]
diff --git a/src/terse/policy.py b/src/terse/policy.py
index 9571ed2..fa166e5 100644
--- a/src/terse/policy.py
+++ b/src/terse/policy.py
@@ -110,17 +110,42 @@ def _coerce_tiers(raw: Any, where: str) -> tuple[str, ...]:
return tuple(raw)
+# The keys load_policy understands, per level. Anything else (except an "_"-prefixed
+# comment/annotation key, the convention policy_gen's `_comment`/`_suggested_fields*`
+# already use) is rejected loudly: this file governs what gets rewritten on the wire,
+# so a typo'd key ("polices", "diff_keyframe_intervall") silently reverting to default
+# behavior is a trap, not a convenience.
+_TOP_KEYS = frozenset({"version", "defaults", "policies", "diff", "diff_keyframe_interval"})
+_DEFAULTS_KEYS = frozenset({"tiers"})
+_RULE_KEYS = frozenset({"match", "tiers", "fields"})
+_MATCH_KEYS = frozenset({"tool"})
+
+
+def _reject_unknown_keys(obj: Any, allowed: frozenset[str], where: str) -> None:
+ if not isinstance(obj, dict):
+ raise ValueError(f"{where}: must be an object, got {type(obj).__name__}")
+ unknown = sorted(k for k in obj if k not in allowed and not k.startswith("_"))
+ if unknown:
+ raise ValueError(f"{where}: unknown key(s) {unknown}; allowed: {sorted(allowed)} "
+ "(prefix a key with '_' for a comment/annotation)")
+
+
def load_policy(path: str | Path) -> Policy:
- """Parse + validate a JSON policy file. Raises ValueError on a malformed policy."""
+ """Parse + validate a JSON policy file. Raises ValueError on a malformed policy —
+ including any unknown key (see `_reject_unknown_keys`): fail loudly on a typo
+ rather than silently running with default behavior."""
doc = json.loads(Path(path).read_text(encoding="utf-8"))
+ _reject_unknown_keys(doc, _TOP_KEYS, str(path))
if doc.get("version") != 1:
raise ValueError(f"unsupported policy version: {doc.get('version')!r} (expected 1)")
- default_tiers = _coerce_tiers(
- doc.get("defaults", {}).get("tiers", list(VALID_TIERS)), "defaults"
- )
+ defaults = doc.get("defaults", {})
+ _reject_unknown_keys(defaults, _DEFAULTS_KEYS, "defaults")
+ default_tiers = _coerce_tiers(defaults.get("tiers", list(VALID_TIERS)), "defaults")
rules: list[Rule] = []
for i, r in enumerate(doc.get("policies", [])):
+ _reject_unknown_keys(r, _RULE_KEYS, f"policies[{i}]")
match = r.get("match", {})
+ _reject_unknown_keys(match, _MATCH_KEYS, f"policies[{i}].match")
glob = match.get("tool", "*")
rules.append(Rule(tool_glob=glob, tiers=_coerce_tiers(r.get("tiers", []), f"policies[{i}]"),
fields=r.get("fields", {})))
diff --git a/src/terse/probes.py b/src/terse/probes.py
index 18916be..83e7a7a 100644
--- a/src/terse/probes.py
+++ b/src/terse/probes.py
@@ -23,8 +23,8 @@
from itertools import combinations
from typing import Any
-from .transforms import minify
from .tokenize import count_cl100k, encode_cl100k
+from .transforms import minify
# Reverse-map a captured tool name to its origin server. The capture envelope stores
# only `tool` (#64 Phase 0): the corpus was captured by separate per-server proxies
@@ -234,7 +234,7 @@ def cross_server_overlap(
capped = capped or len(la) > cap_per_pair or len(lb) > cap_per_pair
sa = sorted(la, key=lambda t: t[0])[:cap_per_pair]
sb = sorted(lb, key=lambda t: t[0])[:cap_per_pair]
- for (sha_a, raw_a), (sha_b, raw_b) in zip(sa, sb):
+ for (sha_a, raw_a), (sha_b, raw_b) in zip(sa, sb, strict=False): # pair up to shorter
res = cross_call_overlap(raw_a, raw_b, idf=idf)
if res.get("available"):
rows.append({"server_a": a, "server_b": b, "sha_a": sha_a, "sha_b": sha_b, **res})
diff --git a/src/terse/proxy.py b/src/terse/proxy.py
index d87a5f8..63d9fe2 100644
--- a/src/terse/proxy.py
+++ b/src/terse/proxy.py
@@ -27,13 +27,13 @@
import subprocess
import sys
from collections import OrderedDict
+from collections.abc import Callable, Iterable
from threading import Lock, Thread
-from typing import Any, Callable, Optional, TextIO
+from typing import Any, TextIO
from . import lossy as lossy_mod
from . import policy as policy_mod
-from . import text_diff
-from . import transforms
+from . import text_diff, transforms
from .tokenize import count_cl100k
from .transport import HttpTransport, build_transport
@@ -117,11 +117,11 @@ class Interceptor:
DROPPED_MAX_BYTES = 8 << 20 # 8 MiB
def __init__(self, pol: policy_mod.Policy, debug: bool = False,
- capture: Optional[Callable[[str, str], None]] = None,
- audit: Optional[Callable[[dict], None]] = None,
- store: Optional["OrderedDict[str, Any]"] = None,
- store_lock: Optional[Lock] = None,
- dropped_bytes: Optional[list[int]] = None):
+ capture: Callable[[str, str], None] | None = None,
+ audit: Callable[[dict], None] | None = None,
+ store: OrderedDict[str, Any] | None = None,
+ store_lock: Lock | None = None,
+ dropped_bytes: list[int] | None = None):
self.policy = pol
# id -> (policy_tool, capture_tool): policy_tool drives compression/policy-tier
# lookup and MUST be the bare name the policy's rules match against; capture_tool
@@ -164,7 +164,7 @@ def __init__(self, pol: policy_mod.Policy, debug: bool = False,
# which peer answers it. Default (None) is 100% behavior-preserving for every
# existing single-peer caller: a fresh private OrderedDict + Lock, exactly as
# before this parameter existed.
- self.dropped: "OrderedDict[str, Any]" = store if store is not None else OrderedDict()
+ self.dropped: OrderedDict[str, Any] = store if store is not None else OrderedDict()
# `dropped_bytes` (#5 Half B): a 1-element box, not a plain int, specifically so
# it can be SHARED the same way `store` is. `self.dropped` can be one dict shared
# across N Interceptors, but a plain `self._dropped_bytes = 0` would still be
@@ -203,7 +203,7 @@ def __init__(self, pol: policy_mod.Policy, debug: bool = False,
# for `_local_lock` while still holding `_store_lock`.
self._store_lock = store_lock if store_lock is not None else Lock()
- def note_request(self, line: str, *, tool_name: Optional[str] = None) -> None:
+ def note_request(self, line: str, *, tool_name: str | None = None) -> None:
"""Record id -> tool name for tools/call requests, and the initialize request id
(so its reply can carry the format primer). Side-effect only.
@@ -415,7 +415,7 @@ def _compress_or_diff(self, block: dict, tool: str) -> bool:
return True
return False
- def _augment_initialize(self, msg: dict) -> Optional[str]:
+ def _augment_initialize(self, msg: dict) -> str | None:
"""Prepend the terse format primer to the initialize result's `instructions` (#13),
preserving any the downstream server set. Idempotent. Returns the reserialized
line, or None to forward unchanged."""
@@ -432,7 +432,7 @@ def _augment_initialize(self, msg: dict) -> Optional[str]:
"initialize.instructions\n")
return json.dumps(msg, separators=(",", ":"), ensure_ascii=False)
- def _diff_wire(self, prev: Any, curr: Any, tool: str) -> Optional[str]:
+ def _diff_wire(self, prev: Any, curr: Any, tool: str) -> str | None:
"""The on-the-wire diff envelope, or None if no lossless diff applies. Self-
describing: it names the prior result (already in the model's context) and
carries the changes inline, so the model reconstructs without an out-of-band
@@ -466,7 +466,7 @@ def _text_diff_or_store(self, block: dict, tool: str, text: str) -> bool:
self.last_text[tool] = text
return changed
- def _text_diff_wire(self, prev: str, curr: str, tool: str) -> Optional[str]:
+ def _text_diff_wire(self, prev: str, curr: str, tool: str) -> str | None:
"""Fail-open wrapper mirroring `_diff_wire`, for the CDC text-diff codec."""
try:
return text_diff.text_diff_wire(prev, curr, tool)
@@ -507,7 +507,7 @@ def _drop_put(self, handle: str, value: Any) -> None:
_, evicted = self.dropped.popitem(last=False)
self._dropped_bytes_box[0] -= len(lossy_mod._serialize(evicted))
- def _inject_retrieve_tool(self, msg: dict) -> Optional[str]:
+ def _inject_retrieve_tool(self, msg: dict) -> str | None:
"""If `msg` is a tools/list result, append the synthetic terse.retrieve tool so the
model can fetch a drop-to-retrieve field back by handle (#10). Idempotent. Returns
the reserialized line, or None to forward unchanged (not a tools/list, or already
@@ -525,7 +525,7 @@ def _inject_retrieve_tool(self, msg: dict) -> Optional[str]:
sys.stderr.write(f"[terse-proxy] injected {lossy_mod.RETRIEVE_TOOL} into tools/list\n")
return json.dumps(msg, separators=(",", ":"), ensure_ascii=False)
- def answer_retrieve(self, line: str) -> Optional[str]:
+ def answer_retrieve(self, line: str) -> str | None:
"""If `line` is a client tools/call for the synthetic terse.retrieve tool, produce the
JSON-RPC reply here — from the drop store — instead of forwarding it downstream, which
has no such tool (#10). Returns the reply line to write back to the client, or None if
@@ -542,6 +542,8 @@ def answer_retrieve(self, line: str) -> Optional[str]:
return None
mid = msg.get("id")
handle = (params.get("arguments") or {}).get("handle")
+ if not isinstance(handle, str):
+ handle = "" # a malformed/absent handle can only ever be a miss below
value = None
with self._store_lock:
hit = handle in self.dropped
@@ -565,7 +567,7 @@ def answer_retrieve(self, line: str) -> Optional[str]:
def _emit_audit(self, tool: str, mid: Any, raw_texts: list[str],
text_blocks: list[dict], changed: bool, *,
- display_tool: Optional[str] = None) -> None:
+ display_tool: str | None = None) -> None:
"""Hand the audit callback one replay record per result (#23). Strictly a side
effect: any error is swallowed so an audit-log write can never change what the
client receives — same fail-open contract as capture.
@@ -582,10 +584,13 @@ def _emit_audit(self, tool: str, mid: Any, raw_texts: list[str],
"tiers": list(self.policy.select(tool).tiers),
"changed": changed,
"blocks": [{"raw": raw, "emitted": b["text"]}
- for raw, b in zip(raw_texts, text_blocks)],
+ for raw, b in zip(raw_texts, text_blocks, strict=True)],
}
+ audit = self.audit
+ if audit is None:
+ return # caller already gates on this; kept for local type-narrowing too
try:
- self.audit(record) # type: ignore[misc] — only called when set
+ audit(record)
except Exception as exc: # noqa: BLE001 — audit is never load-bearing
if self.debug:
sys.stderr.write(f"[terse-proxy] {shown_tool}: audit skipped: {exc}\n")
@@ -597,8 +602,8 @@ def _emit_audit(self, tool: str, mid: Any, raw_texts: list[str],
SWALLOW: Any = object()
-def pump(src: TextIO, dst: TextIO, transform: Callable[[str], Any],
- lock: "Optional[Lock]" = None) -> None:
+def pump(src: Iterable[str], dst: Any, transform: Callable[[str], Any],
+ lock: Lock | None = None) -> None:
"""Read lines from src, apply transform, write to dst with a single trailing newline.
transform returns: a string to write, None to forward the line unchanged, or SWALLOW to
write nothing (the transform handled it out-of-band). Stops at EOF. With `lock`, each
@@ -622,7 +627,7 @@ def pump(src: TextIO, dst: TextIO, transform: Callable[[str], Any],
dst.flush()
-def stdio_transport_error(cmd: list[str]) -> Optional[str]:
+def stdio_transport_error(cmd: list[str]) -> str | None:
"""Return a clear error if `cmd` can't be a proxy downstream target at all, else
None (#19). Currently the only such case is nothing given after `--`. A URL is no
longer rejected here — `transport.build_transport` dispatches a single `"://"`
@@ -633,7 +638,7 @@ def stdio_transport_error(cmd: list[str]) -> Optional[str]:
return None
-def _terminate_child(proc: "subprocess.Popen[Any]", timeout: float = 2.0) -> None:
+def _terminate_child(proc: subprocess.Popen[Any], timeout: float = 2.0) -> None:
"""Reap the downstream server if it is still running, so it shares the proxy's
lifecycle and is never orphaned (#21). SIGTERM first, then SIGKILL on timeout."""
if proc.poll() is not None:
@@ -700,15 +705,15 @@ def _restore_sigterm(token: Any) -> None:
def _build_capture_and_audit(
- capture_dir: Optional[str], debug_log: Optional[str], debug: bool, log_prefix: str
-) -> tuple[Optional[Callable[[str, str], None]], Optional[Callable[[dict], None]]]:
+ capture_dir: str | None, debug_log: str | None, debug: bool, log_prefix: str
+) -> tuple[Callable[[str, str], None] | None, Callable[[dict], None] | None]:
"""Build the (capture, audit) callback pair from --capture-dir/--debug-log, shared
by `run_proxy` and `multiproxy.run_multi_proxy` (identical logic, differing only in
which process's downstream target they're wired to). Both callbacks are strictly
side effects — a read-only or full disk must never break the proxy — so a failure
is swallowed with only a --debug-gated stderr line, tagged with `log_prefix` (e.g.
`"[terse-proxy]"` vs `"[terse-multiproxy]"`) so the failure's origin stays legible."""
- capture: Optional[Callable[[str, str], None]] = None
+ capture: Callable[[str, str], None] | None = None
if capture_dir is not None:
from .capture import capture_payload
@@ -719,7 +724,7 @@ def capture(tool: str, raw: str) -> None:
if debug:
sys.stderr.write(f"{log_prefix} capture_payload failed: {exc}\n")
- audit: Optional[Callable[[dict], None]] = None
+ audit: Callable[[dict], None] | None = None
if debug_log is not None:
from .capture import append_audit
@@ -737,11 +742,11 @@ def run_proxy(
cmd: list[str],
pol: policy_mod.Policy,
debug: bool = False,
- stdin: Optional[TextIO] = None,
- stdout: Optional[TextIO] = None,
- capture_dir: Optional[str] = None,
- debug_log: Optional[str] = None,
- headers: Optional[dict[str, str]] = None,
+ stdin: TextIO | None = None,
+ stdout: TextIO | None = None,
+ capture_dir: str | None = None,
+ debug_log: str | None = None,
+ headers: dict[str, str] | None = None,
) -> int:
"""Launch the downstream MCP peer `cmd` and proxy JSON-RPC through `Interceptor`.
`cmd` is either a stdio launch command, or a single-element list holding a URL — in
diff --git a/src/terse/terminal_report.py b/src/terse/terminal_report.py
index 365b609..0d279f1 100644
--- a/src/terse/terminal_report.py
+++ b/src/terse/terminal_report.py
@@ -13,9 +13,17 @@
import os
import sys
+from collections.abc import Sequence
from typing import Any
-from .report import _GAP_TOLERANCE, _ci, _sum, diff_gap_rows, dropeval_gap_rows, fluency_gap_rows
+from .report import (
+ _GAP_TOLERANCE,
+ _ci,
+ _sum,
+ diff_gap_rows,
+ dropeval_gap_rows,
+ fluency_gap_rows,
+)
_BAR_WIDTH = 24
_BLOCK = "█"
@@ -55,7 +63,7 @@ def diverging_bar_lines(items: list[tuple[str, float]], unit: str = "%",
return "\n".join(lines)
-def stacked_bar_lines(items: list[tuple[str, list[float]]], series_labels: tuple[str, ...],
+def stacked_bar_lines(items: Sequence[tuple[str, Sequence[float]]], series_labels: tuple[str, ...],
series_sgr: tuple[str, ...] = ("34", "32", "33"),
color: bool | None = None) -> str:
"""One row per item: proportional multi-color bar across series_labels, sized by
@@ -69,13 +77,13 @@ def stacked_bar_lines(items: list[tuple[str, list[float]]], series_labels: tuple
enabled = _color_enabled() if color is None else color
label_w = min(max((len(label) for label, _ in items), default=0), 28)
legend = " " + " ".join(
- f"{_c(sgr, _BLOCK, enabled)} {name}" for sgr, name in zip(series_sgr, series_labels)
+ f"{_c(sgr, _BLOCK, enabled)} {name}" for sgr, name in zip(series_sgr, series_labels, strict=True)
)
lines = [legend]
for label, vals in items:
denom = sum(abs(v) for v in vals) or 1
segs, used = [], 0
- for sgr, v in zip(series_sgr, vals):
+ for sgr, v in zip(series_sgr, vals, strict=True):
n = round(abs(v) / denom * _BAR_WIDTH)
used += n
glyph = _BLOCK if v >= 0 else _NEG_BLOCK
@@ -136,7 +144,7 @@ def trend_sparkline_lines(runs: list[dict[str, Any]]) -> str:
regressing" without reading report.build_trend_report's full table. A flat
reading (all bars level) with real historical data is itself a legitimate,
useful signal (a stable win), not a sign something's broken."""
- pcts = [r.get("saved_pct") for r in runs if r.get("saved_pct") is not None]
+ pcts = [float(r["saved_pct"]) for r in runs if r.get("saved_pct") is not None]
if len(pcts) < 2:
return " (need at least two --history runs to show a trend)"
lo, hi = min(pcts), max(pcts)
diff --git a/src/terse/text_diff.py b/src/terse/text_diff.py
index a4c1bcc..1f6792a 100644
--- a/src/terse/text_diff.py
+++ b/src/terse/text_diff.py
@@ -107,13 +107,13 @@ def _flush() -> None:
literal.clear()
for ch in curr_chunks:
- idx = by_fp.get(_fingerprint(ch))
- if idx is not None and prev_chunks[idx] == ch:
+ ref = by_fp.get(_fingerprint(ch))
+ if ref is not None and prev_chunks[ref] == ch:
_flush()
- if ops and ops[-1][0] == "=" and ops[-1][2] == idx - 1:
- ops[-1][2] = idx
+ if ops and ops[-1][0] == "=" and ops[-1][2] == ref - 1:
+ ops[-1][2] = ref
else:
- ops.append(["=", idx, idx])
+ ops.append(["=", ref, ref])
continue
literal.append(ch)
_flush()
diff --git a/src/terse/tokenize.py b/src/terse/tokenize.py
index f26ebd3..c044dc5 100644
--- a/src/terse/tokenize.py
+++ b/src/terse/tokenize.py
@@ -8,7 +8,6 @@
from __future__ import annotations
from functools import lru_cache
-from typing import Optional
CL100K = "cl100k_base" # GPT-3.5/4 — headroom-eval parity
O200K = "o200k_base" # GPT-4o — second, very different vocab for invariance checks
@@ -24,19 +23,19 @@ def _enc(name: str):
return None
-def count(text: str, encoding: str = CL100K) -> Optional[int]:
+def count(text: str, encoding: str = CL100K) -> int | None:
"""Token count under a named tiktoken encoding, or None if unavailable."""
enc = _enc(encoding)
return len(enc.encode(text)) if enc is not None else None
-def count_cl100k(text: str) -> Optional[int]:
+def count_cl100k(text: str) -> int | None:
"""cl100k_base token count. NOT the consumer tokenizer (no public Claude
tokenizer exists); see the cross-tokenizer invariance check for robustness."""
return count(text, CL100K)
-def encode_cl100k(text: str) -> Optional[list[int]]:
+def encode_cl100k(text: str) -> list[int] | None:
"""cl100k_base token ids, or None if unavailable. Used by the probes to reason
about token-level overlap/redundancy, not just counts."""
enc = _enc(CL100K)
diff --git a/src/terse/transforms.py b/src/terse/transforms.py
index 2f3f1fd..509bad8 100644
--- a/src/terse/transforms.py
+++ b/src/terse/transforms.py
@@ -360,7 +360,7 @@ def _locate_records(obj: Any) -> tuple[Any, list[dict]] | None:
def _diff_id_col(prev_recs: list[dict], curr_recs: list[dict]) -> str | None:
"""A column present in every record of both lists whose values are scalar (str/int)
and unique within each list — usable to align rows across the two calls."""
- for c in prev_recs[0].keys():
+ for c in prev_recs[0]:
if not (all(c in r for r in prev_recs) and all(c in r for r in curr_recs)):
continue
pv = [r[c] for r in prev_recs]
diff --git a/src/terse/transport.py b/src/terse/transport.py
index ef4c2fe..33ab740 100644
--- a/src/terse/transport.py
+++ b/src/terse/transport.py
@@ -19,8 +19,9 @@
import subprocess
import urllib.error
import urllib.request
+from collections.abc import Iterator
+from typing import Any, Protocol, TextIO
from urllib.parse import urlsplit
-from typing import Any, Iterator, Optional, Protocol, TextIO
# The only downstream URL schemes terse will dial. `urllib.request.urlopen` also
# honors `file://`, `ftp://`, `data:` and more — so an unrestricted scheme turns a
@@ -30,6 +31,19 @@
# project-scoped `.mcp.json` (see install_mcp.py), so this is not purely operator input.
_ALLOWED_URL_SCHEMES = ("http", "https")
+# Hosts where cleartext http is safe (it never leaves the machine) — the same set
+# fluency.openai_answerer's TLS guard exempts, for the same local-gateway use case.
+_LOOPBACK_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"})
+
+# Header names that carry credentials. A downstream with one of these over cleartext
+# http to a REMOTE host would put the secret on the wire unencrypted — refuse at
+# construction, mirroring fluency's answerer guard (parity noted in the 07-14 audit).
+_SENSITIVE_HEADER_TOKENS = ("authorization", "token", "secret", "key", "cookie")
+
+
+def _has_sensitive_header(headers: dict[str, str]) -> bool:
+ return any(t in name.lower() for name in headers for t in _SENSITIVE_HEADER_TOKENS)
+
class Transport(Protocol):
"""One downstream MCP peer, abstracted over its wire transport.
@@ -122,7 +136,7 @@ class _HttpSendWriter:
differently-behaved caller still gets one POST per JSON-RPC line rather
than one POST of concatenated lines."""
- def __init__(self, transport: "HttpTransport"):
+ def __init__(self, transport: HttpTransport):
self._transport = transport
self._buf = ""
@@ -207,7 +221,7 @@ class HttpTransport:
in-flight request's id and enqueues THAT instead, so the client always gets
a reply rather than silence."""
- def __init__(self, url: str, headers: Optional[dict[str, str]] = None, timeout: int = 60):
+ def __init__(self, url: str, headers: dict[str, str] | None = None, timeout: int = 60):
scheme = urlsplit(url).scheme.lower()
if scheme not in _ALLOWED_URL_SCHEMES:
# Raised at construction (before any I/O), so build_transport's callers
@@ -220,13 +234,21 @@ def __init__(self, url: str, headers: Optional[dict[str, str]] = None, timeout:
"read or SSRF vector)")
self.url = url
self.headers = dict(headers or {})
+ split = urlsplit(url)
+ if (scheme == "http" and split.hostname not in _LOOPBACK_HOSTS
+ and _has_sensitive_header(self.headers)):
+ # Same construction-time, before-any-I/O contract as the scheme check above.
+ raise ValueError(
+ f"terse: refusing to send credential header(s) over cleartext http to "
+ f"{split.hostname!r} — use https, or a loopback gateway "
+ f"({'/'.join(sorted(_LOOPBACK_HOSTS))})")
self.timeout = timeout
- self._q: "queue.Queue[Any]" = queue.Queue()
+ self._q: queue.Queue[Any] = queue.Queue()
# MCP Streamable HTTP session affinity: some servers pin a client to
# server-side state via this header, set on a prior response. Captured
# opportunistically and echoed back on every subsequent POST — never
# required, since plenty of servers don't use it at all.
- self.session: Optional[str] = None
+ self.session: str | None = None
def inbound(self) -> Iterator[str]:
return iter(self._q.get, _SENTINEL)
@@ -339,7 +361,7 @@ def _error_reply(mid: Any, message: str) -> str:
)
-def build_transport(target: list[str], *, headers: Optional[dict[str, str]] = None) -> Transport:
+def build_transport(target: list[str], *, headers: dict[str, str] | None = None) -> Transport:
"""Build the right `Transport` for a proxy `cmd`/downstream target.
A single element containing `"://"` is a URL -> `HttpTransport`; anything
diff --git a/tests/test_fluency.py b/tests/test_fluency.py
index 9eb41e9..9b27c18 100644
--- a/tests/test_fluency.py
+++ b/tests/test_fluency.py
@@ -554,9 +554,9 @@ def test_build_chain_windows_yields_every_depth_and_valid_hops():
windows = fluency.build_chain_windows(_soak_envs(n=8), max_depth=3, per_depth_cap=4)
depths = {d for _, _, d, _ in windows}
assert depths == {1, 2, 3}
- for tool, sha, depth, objs in windows:
+ for tool, _sha, depth, objs in windows:
assert len(objs) == depth + 1
- for prev, curr in zip(objs, objs[1:]):
+ for prev, curr in zip(objs, objs[1:], strict=False):
assert diff_wire(prev, curr, tool) is not None # every hop truly chains
assert fluency.gen_questions(objs[-1]) # final state is askable
@@ -570,7 +570,7 @@ def test_build_chain_windows_never_spans_a_diff_break():
windows = fluency.build_chain_windows(envs, max_depth=3, per_depth_cap=8)
from terse.transforms import diff_wire
for tool, _sha, _depth, objs in windows:
- for prev, curr in zip(objs, objs[1:]):
+ for prev, curr in zip(objs, objs[1:], strict=False):
assert diff_wire(prev, curr, tool) is not None
diff --git a/tests/test_install_mcp.py b/tests/test_install_mcp.py
index a33a3fa..4ded1ea 100644
--- a/tests/test_install_mcp.py
+++ b/tests/test_install_mcp.py
@@ -587,3 +587,70 @@ def test_scan_scopes_is_read_only(tmp_path, monkeypatch):
assert cfg.read_text() == before
assert not im.stash_path(cfg).exists()
assert list(tmp_path.glob("*.bak-*")) == []
+
+
+def test_rewrap_preserves_hand_edits_on_wrapped_entry():
+ # The 2026-07-13 production incident: a scoped env.PATH pin hand-added to the
+ # WRAPPED entry was silently reverted by a re-install, because wrap() rebuilt
+ # purely from the stashed (pre-pin) original. The drift guard keeps live
+ # non-terse-owned keys on a re-wrap.
+ config = _cfg(codegraph={"command": "/usr/local/bin/codegraph",
+ "args": ["serve", "--mcp"], "type": "stdio"})
+ stash: dict = {}
+ im.wrap(config, stash, "codegraph", "/p/policy.json", TERSE_CMD)
+ # operator pins node@22 on the wrapped entry by hand
+ config["mcpServers"]["codegraph"]["env"] = {"PATH": "/opt/node22/bin:/usr/bin"}
+
+ im.wrap(config, stash, "codegraph", "/p/policy.json", TERSE_CMD, diff=False)
+ entry = config["mcpServers"]["codegraph"]
+ assert entry["env"] == {"PATH": "/opt/node22/bin:/usr/bin"} # pin survived
+ assert "--no-diff" in entry["args"] # flags still rebuilt
+ assert entry["command"] == "/abs/python" # command still terse's
+
+ # a live hand-edit also WINS over the stashed original's value for the same key
+ config["mcpServers"]["codegraph"]["type"] = "http" # hand-changed
+ im.wrap(config, stash, "codegraph", "/p/policy.json", TERSE_CMD)
+ assert config["mcpServers"]["codegraph"]["type"] == "http"
+
+ # the guard never leaks the hand-edit into the stash: uninstall restores pristine
+ im.unwrap(config, stash, "codegraph")
+ assert config["mcpServers"]["codegraph"] == {
+ "command": "/usr/local/bin/codegraph", "args": ["serve", "--mcp"], "type": "stdio"}
+
+
+def test_rewrap_never_resurrects_url_headers_from_a_drifted_live_entry():
+ # If someone hand-replaces a managed server's live entry with a raw url entry,
+ # a re-wrap must not copy url/headers onto the wrapped shape (an entry with both
+ # args and url is broken) — those keys are always folded into args from the stash.
+ original = {"url": "https://example.com/mcp", "headers": {"X": "1"}}
+ config = _cfg(remote=dict(original))
+ stash: dict = {}
+ im.wrap(config, stash, "remote", "/p/policy.json", TERSE_CMD)
+ config["mcpServers"]["remote"] = dict(original) # hand-reverted
+ im.wrap(config, stash, "remote", "/p/policy.json", TERSE_CMD)
+ entry = config["mcpServers"]["remote"]
+ assert "url" not in entry and "headers" not in entry
+ assert entry["args"][-1] == "https://example.com/mcp"
+
+
+def test_do_install_reports_preserved_hand_edits(tmp_path, monkeypatch):
+ cfg = tmp_path / ".claude.json"
+ cfg.write_text(json.dumps(_cfg(runecho={"command": "uvx", "args": ["runecho-mcp"]})))
+ policy = tmp_path / "policy.json"
+ policy.write_text("{}")
+ monkeypatch.setattr(im, "terse_invocation", lambda: TERSE_CMD)
+
+ im.do_install(["runecho"], str(policy), cfg=cfg)
+ written = json.loads(cfg.read_text())
+ written["mcpServers"]["runecho"]["env"] = {"PATH": "/pin"} # hand-edit
+ cfg.write_text(json.dumps(written))
+
+ res = im.do_install(["runecho"], str(policy), cfg=cfg)
+ change = res["changes"][0]
+ assert change["preserved"] == ["env"]
+ assert json.loads(cfg.read_text())["mcpServers"]["runecho"]["env"] == {"PATH": "/pin"}
+ # the edit stays live-only (never leaks into the stash), so EVERY later re-wrap
+ # keeps carrying — and keeps reporting — it; that persistence is the guard working
+ res = im.do_install(["runecho"], str(policy), cfg=cfg)
+ assert res["changes"][0]["preserved"] == ["env"]
+ assert json.loads(cfg.read_text())["mcpServers"]["runecho"]["env"] == {"PATH": "/pin"}
diff --git a/tests/test_multiproxy.py b/tests/test_multiproxy.py
index e1da232..bb1bc7c 100644
--- a/tests/test_multiproxy.py
+++ b/tests/test_multiproxy.py
@@ -242,7 +242,7 @@ def test_shared_drop_store_across_peers(tmp_path):
gh_transport = build_transport([sys.executable, str(FAKE)])
http_transport = build_transport([_url(srv)])
try:
- store: "OrderedDict[str, object]" = OrderedDict()
+ store: OrderedDict[str, object] = OrderedDict()
store_lock = Lock()
gh_inter = Interceptor(DROP_POLICY, store=store, store_lock=store_lock)
http_inter = Interceptor(DROP_POLICY, store=store, store_lock=store_lock)
@@ -384,7 +384,7 @@ def test_interceptor_default_store_is_private_and_unaffected():
def test_interceptor_injected_store_is_actually_shared():
- store: "OrderedDict[str, object]" = OrderedDict()
+ store: OrderedDict[str, object] = OrderedDict()
lock = Lock()
a = Interceptor(DROP_POLICY, store=store, store_lock=lock)
b = Interceptor(DROP_POLICY, store=store, store_lock=lock)
@@ -401,7 +401,7 @@ def test_shared_dropped_bytes_evicts_over_combined_cap_across_peers():
# DICT was shared (multiproxy._build_peers), so the DROPPED_MAX_BYTES cap never saw
# the true combined size — two peers each individually under-cap could jointly blow
# way past it. A shared `dropped_bytes` box fixes that.
- store: "OrderedDict[str, object]" = OrderedDict()
+ store: OrderedDict[str, object] = OrderedDict()
lock = Lock()
dropped_bytes: list[int] = [0]
a = Interceptor(DROP_POLICY, store=store, store_lock=lock, dropped_bytes=dropped_bytes)
@@ -500,7 +500,7 @@ def test_build_peers_diff_override_reaches_peer_with_own_policy_path(monkeypatch
monkeypatch.setattr(mp, "build_transport",
lambda target, headers=None: _FakePeerTransport())
own_policy = tmp_path / "own.json"
- own_policy.write_text(json.dumps({"version": 1, "rules": []}), encoding="utf-8")
+ own_policy.write_text(json.dumps({"version": 1, "policies": []}), encoding="utf-8") # ("rules" was a schema typo the loader used to swallow — now rejected)
specs = [
DownstreamSpec(name="a", target=["a"], headers={}, policy_path=None),
DownstreamSpec(name="b", target=["b"], headers={}, policy_path=str(own_policy)),
diff --git a/tests/test_policy.py b/tests/test_policy.py
index d672781..ecb7e54 100644
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -145,3 +145,37 @@ def test_unsupported_version_rejected(tmp_path):
bad.write_text(json.dumps({"version": 2}))
with pytest.raises(ValueError):
load_policy(bad)
+
+
+def test_load_policy_rejects_unknown_keys_at_every_level(tmp_path):
+ # A typo'd key silently reverting to default behavior is a trap — the loader
+ # rejects unknown keys loudly at every level (audit fix #3). "_"-prefixed
+ # annotation keys (policy_gen's _comment/_suggested_fields*) stay exempt.
+ import pytest
+
+ from terse.policy import load_policy
+
+ def _write(doc):
+ p = tmp_path / "p.json"
+ p.write_text(json.dumps(doc))
+ return p
+
+ base = {"version": 1, "defaults": {"tiers": ["minify"]}, "policies": []}
+
+ for doc, needle in [
+ ({**base, "polices": []}, "polices"), # top-level typo
+ ({**base, "diff_keyframe_intervall": 3}, "intervall"), # top-level typo
+ ({**base, "defaults": {"tiers": ["minify"], "teirs": []}}, "teirs"),
+ ({**base, "policies": [{"match": {"tool": "x"}, "tiers": [], "feilds": {}}]}, "feilds"),
+ ({**base, "policies": [{"match": {"tool": "x", "name": "y"}, "tiers": []}]}, "name"),
+ ]:
+ with pytest.raises(ValueError, match=needle):
+ load_policy(_write(doc))
+
+ # underscore-prefixed annotations pass at every level (the policy_gen convention)
+ ok = {"version": 1, "_comment": "hi",
+ "defaults": {"tiers": ["minify"], "_note": "x"},
+ "policies": [{"_comment": "c", "match": {"tool": "x", "_why": "w"},
+ "tiers": [], "_suggested_fields": {"a": {}}}]}
+ pol = load_policy(_write(ok))
+ assert pol.rules[0].tool_glob == "x"
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 2fab8ba..42ba72a 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -13,11 +13,11 @@
import json
import threading
+import pytest
+
from terse import transforms
from terse.lossy import _handle, _serialize
from terse.policy import Policy, Rule
-import pytest
-
from terse.proxy import Interceptor, run_proxy
from terse.transport import HttpTransport, build_transport
@@ -113,7 +113,7 @@ def _send_sse(self, mid) -> None:
"result": {"content": [{"type": "text", "text": "first"}]}})
ev2 = json.dumps({"jsonrpc": "2.0", "method": "notifications/progress",
"params": {"pct": 50}})
- payload = f"data: {ev1}\n\ndata: {ev2}\n\n".encode("utf-8")
+ payload = f"data: {ev1}\n\ndata: {ev2}\n\n".encode()
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(payload)))
@@ -376,3 +376,19 @@ def test_run_proxy_rejects_file_url_scheme_as_config_error(capsys):
rc = run_proxy(["file:///etc/passwd"], FULL, stdin=io.StringIO(""), stdout=io.StringIO())
assert rc == 2
assert "not allowed" in capsys.readouterr().err
+
+
+def test_http_transport_refuses_credential_headers_over_remote_cleartext():
+ # Parity with fluency.openai_answerer's TLS guard (audit fix #3): a Bearer/token
+ # header over http to a remote host puts the credential on the wire unencrypted.
+ import pytest
+
+ for name in ("Authorization", "X-Api-Key", "Proxy-Token", "Cookie", "client-secret"):
+ with pytest.raises(ValueError, match="cleartext http"):
+ HttpTransport("http://api.example.com/mcp", headers={name: "v"})
+ # https, loopback http, and non-sensitive headers over http all still construct
+ assert HttpTransport("https://api.example.com/mcp", headers={"Authorization": "v"})
+ assert HttpTransport("http://127.0.0.1:4000/mcp", headers={"Authorization": "v"})
+ assert HttpTransport("http://localhost:4000/mcp", headers={"X-Api-Key": "v"})
+ assert HttpTransport("http://api.example.com/mcp", headers={"X-Trace-Id": "v"})
+ assert HttpTransport("http://api.example.com/mcp")
diff --git a/uv.lock b/uv.lock
index 92dd750..ef034ab 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,6 +1,51 @@
version = 1
revision = 3
requires-python = ">=3.11"
+resolution-markers = [
+ "python_full_version >= '3.15'",
+ "python_full_version < '3.15'",
+]
+
+[[package]]
+name = "ast-serialize"
+version = "0.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" },
+ { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" },
+ { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" },
+ { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" },
+ { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" },
+ { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" },
+ { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" },
+ { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" },
+ { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" },
+ { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" },
+ { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" },
+ { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" },
+]
[[package]]
name = "certifi"
@@ -127,6 +172,142 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
+[[package]]
+name = "librt"
+version = "0.13.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029, upload-time = "2026-07-08T12:24:45.961Z" },
+ { url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036, upload-time = "2026-07-08T12:24:47.257Z" },
+ { url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062, upload-time = "2026-07-08T12:24:48.483Z" },
+ { url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510, upload-time = "2026-07-08T12:24:49.727Z" },
+ { url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909, upload-time = "2026-07-08T12:24:51.135Z" },
+ { url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620, upload-time = "2026-07-08T12:24:52.358Z" },
+ { url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363, upload-time = "2026-07-08T12:24:53.503Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209, upload-time = "2026-07-08T12:24:55.138Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254, upload-time = "2026-07-08T12:24:56.276Z" },
+ { url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611, upload-time = "2026-07-08T12:24:57.561Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906, upload-time = "2026-07-08T12:24:58.806Z" },
+ { url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852, upload-time = "2026-07-08T12:25:00.065Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832, upload-time = "2026-07-08T12:25:01.148Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" },
+ { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" },
+ { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" },
+ { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" },
+ { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" },
+ { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" },
+ { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" },
+ { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" },
+ { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" },
+ { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" },
+ { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" },
+ { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" },
+ { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" },
+ { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" },
+ { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" },
+ { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ast-serialize" },
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/b9/d75b3082b05f1b3028828aeb18e74ae5ab0a0936051bbf1f32f59f654747/mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", size = 14838725, upload-time = "2026-07-13T11:32:44.655Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/50/79a65c6ea6e115bc73296038a4543b2d5c91f07912b918a2c616a2514bba/mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", size = 13911128, upload-time = "2026-07-13T11:32:02.021Z" },
+ { url = "https://files.pythonhosted.org/packages/90/48/e11ed7716c26953ca321f726e452e374dbf81a6f2b8b212ec02af29b6b8f/mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", size = 14146742, upload-time = "2026-07-13T11:33:03.313Z" },
+ { url = "https://files.pythonhosted.org/packages/06/72/6807565b1c4861ef66f7fdd98b51c61556356eab80235717b46c53bb8627/mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", size = 15081418, upload-time = "2026-07-13T11:31:13.899Z" },
+ { url = "https://files.pythonhosted.org/packages/00/80/1ea14c5d80e589e415973db3e47c78c2219a305b808b2b506395342c1d79/mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", size = 15328164, upload-time = "2026-07-13T11:31:35.723Z" },
+ { url = "https://files.pythonhosted.org/packages/37/28/8223157404a3d51920078459c37f80fbdc590e1d8ea049dc5ce48643022a/mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", size = 11136472, upload-time = "2026-07-13T11:27:37.018Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/cc/ea27e5959c5f258585a756b252031f3b313583d81b5064b2bebc41d3706b/mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", size = 10135800, upload-time = "2026-07-13T11:30:08.92Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" },
+ { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" },
+ { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" },
+ { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" },
+ { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" },
+ { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" },
+ { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" },
+ { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" },
+ { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" },
+ { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" },
+ { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" },
+ { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" },
+ { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
[[package]]
name = "packaging"
version = "26.2"
@@ -136,6 +317,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
+[[package]]
+name = "pathspec"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
+]
+
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -324,6 +514,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
+ { name = "mypy" },
{ name = "pytest" },
{ name = "ruff" },
]
@@ -333,6 +524,7 @@ requires-dist = [{ name = "tiktoken", specifier = ">=0.7" }]
[package.metadata.requires-dev]
dev = [
+ { name = "mypy" },
{ name = "pytest" },
{ name = "ruff" },
]
@@ -391,6 +583,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" },
]
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
[[package]]
name = "urllib3"
version = "2.7.0"