From fb2029e50a4b97e53d7502bc63ff6cf5ef1d6398 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 07:45:46 -0500 Subject: [PATCH 01/71] mcp(feat[mcp_swap]): Target a pull request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Reviewing a branch across the agent CLIs meant checking it out, swapping to the checkout, then remembering to unwind both. uv resolves a git ref on its own, so a pull request can be swapped in without a working copy at all — which makes reverting the ordinary config restore, with nothing left on disk to prune. Resolution happens when an agent starts the server, so a bad ref would otherwise land in every config and fail opaquely inside each one. The swap now proves the command answers MCP before writing anything. what: - Add `use-local --pr N`, writing `uvx --from @refs/pull/N/head` - Complete an MCP initialize round trip before the first write, with `--no-preflight` to skip it - Read the pull request through `gh` to confirm it exists and label the output, keeping resolution independent of it - Recognize the shape in `status`, ahead of the version-pin branch that would otherwise report the ref as a pin --- CHANGES | 10 ++ scripts/README.md | 37 ++++++ scripts/mcp_swap.py | 247 ++++++++++++++++++++++++++++++++++++++++- tests/test_mcp_swap.py | 174 +++++++++++++++++++++++++++++ 4 files changed, 463 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index bd33a90a..2a988e60 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,16 @@ or as a bare name carrying only its type. ### Development +**`mcp_swap` can point every agent CLI at a pull request** + +`use-local --pr N` rewrites each CLI's entry to run the pull request's head +through `uvx` instead of a working copy. Nothing is checked out, so reverting +is the ordinary config restore with no worktree to prune, and a pull request +from a fork needs no special handling. The swap completes an MCP `initialize` +round trip against the resolved command first, so a ref that does not exist — +or a dependency that cannot resolve — fails before it reaches any config +rather than surfacing as an opaque startup error inside every agent. + #### CI actions updated to current majors Workflow actions moved to their current major releases: `actions/checkout` v7, diff --git a/scripts/README.md b/scripts/README.md index 08601f7d..1588f3c0 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -46,6 +46,43 @@ This matches Claude's conventional dev form and takes advantage of `uv run`'s automatic editable install — source edits flow through on the next invocation with no reinstall step. +### `--pr N` — point every CLI at a pull request + +Review a branch across your agents without checking it out: + +```console +$ uv run scripts/mcp_swap.py use-local --pr 114 +``` + +Each CLI's entry becomes: + +``` +command = "uvx" +args = ["--from", "git+@refs/pull/114/head", "libtmux-mcp"] +``` + +`uv` resolves the ref itself, so nothing lands on disk to refresh or +prune and `revert` restores the config with no extra cleanup. GitHub +publishes `refs/pull/N/head` on the base repository, so a pull request +from a fork needs no special handling. + +Before writing anything, the swap launches the resolved command once and +completes an MCP `initialize` round trip. A ref that does not exist, or a +dependency that cannot resolve, fails there — rather than landing in +every CLI's config and surfacing later as an opaque startup error inside +each agent. Pass `--no-preflight` to skip the probe when offline. + +`gh` confirms the number exists and labels the output; resolution does +not depend on it, so an unauthenticated `gh` degrades to an unlabelled +swap rather than a failure. + +A branch whose dependencies need resolver flags can carry them as +environment, the same way any other setting travels: + +```console +$ uv run scripts/mcp_swap.py use-local --pr 114 --env UV_NO_CONFIG=1 +``` + ### `--scope {user,project}` (Claude only) Claude's `~/.claude.json` supports two config scopes for MCP servers: diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 5b4355e4..2a77f06b 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -76,7 +76,9 @@ import json import os import pathlib +import re import shutil +import subprocess import sys import tempfile import time @@ -235,6 +237,12 @@ class CLIInfo: } +#: A ``--from`` argument pointing at a pull request's head commit. +#: GitHub publishes ``refs/pull//head`` on the *base* repository, so +#: one URL serves same-repo and fork pull requests alike. +PR_REF_RE = re.compile(r"git\+(?P.+?)@refs/pull/(?P\d+)/head") + + @dataclasses.dataclass class McpServerSpec: """The portable shape shared across CLI configs.""" @@ -275,6 +283,16 @@ def local_repo_path(self) -> pathlib.Path | None: return None return pathlib.Path(self.args[i + 1]) + def pr_ref(self) -> tuple[str, int] | None: + """Return ``(repo_url, pr_number)`` for a ``uvx`` pull-request spec.""" + if self.command != "uvx": + return None + for arg in self.args: + match = PR_REF_RE.fullmatch(arg) + if match: + return match.group("url"), int(match.group("number")) + return None + @dataclasses.dataclass class SwapEntry: @@ -665,6 +683,161 @@ def build_local_spec(repo: pathlib.Path, entry: str) -> McpServerSpec: ) +def build_pr_spec(repo_url: str, pr: int, entry: str) -> McpServerSpec: + """Build the ``uvx --from git+@refs/pull//head `` spec. + + Nothing is checked out: ``uv`` resolves the ref itself, so a swap + leaves no worktree to refresh or prune and ``revert`` needs no + cleanup beyond restoring the config. + """ + return McpServerSpec( + command="uvx", + args=["--from", f"git+{repo_url}@refs/pull/{pr}/head", entry], + ) + + +def _run_text(argv: list[str], cwd: pathlib.Path | None = None) -> str: + """Run ``argv`` and return stdout, raising on a non-zero exit.""" + return subprocess.run( + argv, + cwd=None if cwd is None else str(cwd), + capture_output=True, + text=True, + check=True, + ).stdout + + +def remote_https_url(repo: pathlib.Path, remote: str = "origin") -> str: + """Return ``https:////`` for a repo's git remote. + + Normalizes the spellings git accepts — ``git@host:owner/name.git``, + an ``ssh://`` or ``git+ssh://`` scheme, an embedded user, a trailing + ``.git`` — because the pull-request ref is fetched over https however + the working copy was cloned. + """ + try: + raw = _run_text(["git", "-C", str(repo), "remote", "get-url", remote]) + except (OSError, subprocess.CalledProcessError) as exc: + msg = f"cannot read git remote {remote!r} in {repo}" + raise RuntimeError(msg) from exc + return _normalize_remote_url(raw.strip()) + + +def _normalize_remote_url(url: str) -> str: + """Rewrite any git remote spelling as a plain https URL. + + Examples + -------- + >>> _normalize_remote_url("git+ssh://git@github.com/o/n.git") + 'https://github.com/o/n' + >>> _normalize_remote_url("git@github.com:o/n.git") + 'https://github.com/o/n' + >>> _normalize_remote_url("https://github.com/o/n") + 'https://github.com/o/n' + """ + url = url.removeprefix("git+") + if url.startswith("ssh://"): + url = "https://" + url.removeprefix("ssh://") + elif "://" not in url and ":" in url: + host, _, path = url.partition(":") + url = f"https://{host}/{path}" + scheme, sep, rest = url.partition("://") + authority, slash, path = rest.partition("/") + return f"{scheme}{sep}{authority.rpartition('@')[2]}{slash}{path}".removesuffix( + ".git" + ) + + +def gh_pr_summary(repo: pathlib.Path, pr: int) -> dict[str, t.Any] | None: + """Return ``gh``'s view of a pull request, or ``None`` when unreadable. + + Used to confirm the number exists and to label output. Resolution + does not depend on it: the ref and URL come from git, so a missing + or unauthenticated ``gh`` degrades to an unlabelled swap rather than + a failure. + """ + try: + out = _run_text( + [ + "gh", + "pr", + "view", + str(pr), + "--json", + "number,title,state,headRefName,isCrossRepository", + ], + cwd=repo, + ) + except (OSError, subprocess.CalledProcessError): + return None + try: + loaded = json.loads(out) + except json.JSONDecodeError: + return None + return loaded if isinstance(loaded, dict) else None + + +#: One MCP ``initialize`` request, newline-framed for stdio. +_INITIALIZE_FRAME = ( + json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "clientInfo": {"name": "mcp_swap-preflight", "version": "1"}, + }, + } + ) + + "\n" +) + + +def preflight_spec(spec: McpServerSpec, *, timeout: float = 300.0) -> str | None: + """Launch ``spec`` and complete one MCP ``initialize`` round trip. + + Returns ``None`` when the server answered, otherwise a reason to + show the operator. A pull-request spec resolves its dependencies at + launch time, inside whichever agent starts it, so an unresolvable + ref would otherwise land in every config and surface later as an + opaque startup failure in each one. + + Closing stdin after the frame lets a well-behaved stdio server exit + on its own, which keeps this free of signal handling. + """ + try: + proc = subprocess.Popen( + [spec.command, *spec.args], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={**os.environ, **spec.env}, + text=True, + ) + except OSError as exc: + return f"could not launch {spec.command}: {exc}" + + try: + out, err = proc.communicate(_INITIALIZE_FRAME, timeout=timeout) + except subprocess.TimeoutExpired: + proc.kill() + proc.communicate() + return f"no MCP response within {timeout:.0f}s" + + for line in out.splitlines(): + try: + message = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(message, dict) and message.get("id") == 1 and "result" in message: + return None + + tail = "\n".join(err.strip().splitlines()[-3:]) + return tail or "server exited without answering initialize" + + # --------------------------------------------------------------------------- # State file # --------------------------------------------------------------------------- @@ -845,18 +1018,32 @@ def cmd_status(args: argparse.Namespace) -> int: def _describe_spec(spec: McpServerSpec, repo: pathlib.Path) -> str: - """Return a short label classifying a spec (local/pypi-pin/other).""" + """Return a short label classifying a spec (local/PR/pypi-pin/other).""" if spec.is_local_uv_directory(): local = spec.local_repo_path() if local and local.resolve() == repo.resolve(): return "local: this repo" return f"local: {local}" + pr = spec.pr_ref() + if pr is not None: + # Checked before the pin branch below: a PR ref contains `@`, + # which that branch would report as a version pin. + return f"PR #{pr[1]}: {pr[0]}" if spec.command == "uvx": pinned = next((a for a in spec.args if "==" in a or "@" in a), None) return f"pypi pin: {pinned}" if pinned else "pypi (unpinned)" return "other" +def _points_at( + current: McpServerSpec, target: McpServerSpec, repo: pathlib.Path +) -> bool: + """Return True when ``current`` already runs what ``target`` describes.""" + if target.pr_ref() is not None: + return current.pr_ref() == target.pr_ref() + return current.is_local_uv_directory() and current.local_repo_path() == repo + + def cmd_use_local(args: argparse.Namespace) -> int: """Rewrite each target CLI's config to run the repo's checkout via ``uv``. @@ -868,9 +1055,30 @@ def cmd_use_local(args: argparse.Namespace) -> int: server, default_entry = resolve_repo_meta(repo) server = args.server or server entry = args.entry or default_entry - spec = build_local_spec(repo, entry) extra_env = dict(args.env or []) + pr = getattr(args, "pr", None) + if pr is None: + spec = build_local_spec(repo, entry) + else: + try: + spec = build_pr_spec(remote_https_url(repo), pr, entry) + except RuntimeError as exc: + print(exc, file=sys.stderr) + return 1 + spec = dataclasses.replace(spec, env=dict(extra_env)) + summary = gh_pr_summary(repo, pr) + if summary is None: + print(f"PR #{pr}: gh could not read it — swapping anyway", file=sys.stderr) + else: + fork = " (fork)" if summary.get("isCrossRepository") else "" + print( + f"PR #{summary.get('number', pr)} [{summary.get('state', '?')}]" + f"{fork} {summary.get('headRefName', '?')} — " + f"{summary.get('title', '')}", + file=sys.stderr, + ) + hint = _naming_hint(repo, server) if hint: print(hint, file=sys.stderr) @@ -880,6 +1088,15 @@ def cmd_use_local(args: argparse.Namespace) -> int: print("no CLIs detected — nothing to do", file=sys.stderr) return 1 + # Runs under --dry-run too: resolving the ref is the only signal a + # dry run can give about whether the swap would actually start. + if pr is not None and not args.no_preflight: + print(f"preflight: {spec.command} {' '.join(spec.args)}", file=sys.stderr) + failure = preflight_spec(spec) + if failure is not None: + print(f"preflight failed, nothing written:\n{failure}", file=sys.stderr) + return 1 + ts = time.strftime("%Y%m%d%H%M%S") state = load_state() had_error = 0 @@ -901,11 +1118,11 @@ def cmd_use_local(args: argparse.Namespace) -> int: current = get_server(cli, config, server, repo, scope=scope) if ( current - and current.is_local_uv_directory() - and current.local_repo_path() == repo + and _points_at(current, spec, repo) and all(current.env.get(k) == v for k, v in extra_env.items()) ): - print(f"[{label}] already local (this repo) — no change") + where = "local (this repo)" if pr is None else f"PR #{pr}" + print(f"[{label}] already {where} — no change") continue # Preserve the existing entry's env on replacement. ``build_local_spec`` # writes an empty env, so without this merge a swap would silently drop @@ -1320,6 +1537,26 @@ def build_parser() -> argparse.ArgumentParser: pu = sub.add_parser("use-local", help="rewrite configs to run this checkout") pu.add_argument("--repo", default=".", help="repo root (default: .)") + pu.add_argument( + "--pr", + type=int, + metavar="N", + help=( + "Point the CLIs at pull request N instead of the working copy. " + "Writes 'uvx --from git+@refs/pull/N/head ', so " + "nothing is checked out and 'revert' needs no cleanup. The ref " + "lives on the base repo, so fork PRs work unchanged." + ), + ) + pu.add_argument( + "--no-preflight", + action="store_true", + help=( + "Skip the MCP initialize round trip --pr runs before writing. " + "The probe resolves the ref once so a bad PR fails here instead " + "of inside every agent; skip it when offline or already warm." + ), + ) pu.add_argument( "--server", help="MCP server name (default: derived from pyproject.toml)" ) diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index a54139b1..2532e609 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -1969,3 +1969,177 @@ def swap(value: str) -> None: assert "recorded backup is gone" in capsys.readouterr().err fresh = pathlib.Path(mcp_swap.load_state()[("cursor", "user")].backup_path) assert fresh.read_bytes() == swapped_bytes + + +# --------------------------------------------------------------------------- +# Pull-request targeting +# --------------------------------------------------------------------------- + + +class RemoteURLFixture(t.NamedTuple): + """One git remote spelling and the https URL it normalizes to. + + Attributes + ---------- + test_id : str + Identifier shown in the parametrized test name. + remote : str + A URL as ``git remote get-url`` may report it. + expected : str + The https form the pull-request ref is fetched from. + """ + + test_id: str + remote: str + expected: str + + +REMOTE_URL_FIXTURES: list[RemoteURLFixture] = [ + RemoteURLFixture( + "git_ssh_scheme", "git+ssh://git@github.com/o/n.git", "https://github.com/o/n" + ), + RemoteURLFixture( + "ssh_scheme", "ssh://git@github.com/o/n.git", "https://github.com/o/n" + ), + RemoteURLFixture( + "scp_shorthand", "git@github.com:o/n.git", "https://github.com/o/n" + ), + RemoteURLFixture( + "https_dotgit", "https://github.com/o/n.git", "https://github.com/o/n" + ), + RemoteURLFixture("https_plain", "https://github.com/o/n", "https://github.com/o/n"), + RemoteURLFixture( + "self_hosted", + "git@git.example.com:team/n.git", + "https://git.example.com/team/n", + ), +] + + +@pytest.mark.parametrize( + RemoteURLFixture._fields, + REMOTE_URL_FIXTURES, + ids=[f.test_id for f in REMOTE_URL_FIXTURES], +) +def test_normalize_remote_url(test_id: str, remote: str, expected: str) -> None: + """Every spelling git accepts resolves to the same https URL.""" + assert test_id + assert mcp_swap._normalize_remote_url(remote) == expected + + +def test_build_pr_spec_round_trips_through_pr_ref() -> None: + """A built pull-request spec is recognized by the reader that parses it.""" + spec = mcp_swap.build_pr_spec("https://github.com/o/n", 114, "libtmux-mcp") + + assert spec.command == "uvx" + assert spec.args == [ + "--from", + "git+https://github.com/o/n@refs/pull/114/head", + "libtmux-mcp", + ] + assert spec.pr_ref() == ("https://github.com/o/n", 114) + assert spec.is_local_uv_directory() is False + + +def test_pr_ref_ignores_non_pr_specs() -> None: + """A local checkout and a version pin are not pull-request specs.""" + local = mcp_swap.McpServerSpec( + command="uv", args=["--directory", "/tmp", "run", "x"] + ) + pinned = mcp_swap.McpServerSpec(command="uvx", args=["libtmux-mcp==0.1.0a2"]) + branch = mcp_swap.McpServerSpec( + command="uvx", args=["--from", "git+https://github.com/o/n@main", "x"] + ) + + assert local.pr_ref() is None + assert pinned.pr_ref() is None + assert branch.pr_ref() is None + + +def test_describe_spec_labels_a_pr_before_the_version_pin_branch( + tmp_path: pathlib.Path, +) -> None: + """A pull-request ref is described as a PR, not as a version pin. + + The ref carries an ``@``, which the pin branch would otherwise report + as ``pypi pin: git+...@refs/pull/114/head``. + """ + spec = mcp_swap.build_pr_spec("https://github.com/o/n", 114, "libtmux-mcp") + + assert mcp_swap._describe_spec(spec, tmp_path) == "PR #114: https://github.com/o/n" + + +def test_points_at_distinguishes_pr_numbers(tmp_path: pathlib.Path) -> None: + """A swap to one pull request is not treated as already pointing at another.""" + target = mcp_swap.build_pr_spec("https://github.com/o/n", 114, "x") + same = mcp_swap.build_pr_spec("https://github.com/o/n", 114, "x") + other = mcp_swap.build_pr_spec("https://github.com/o/n", 115, "x") + local = mcp_swap.build_local_spec(tmp_path, "x") + + assert mcp_swap._points_at(same, target, tmp_path) is True + assert mcp_swap._points_at(other, target, tmp_path) is False + assert mcp_swap._points_at(local, target, tmp_path) is False + assert mcp_swap._points_at(local, local, tmp_path) is True + + +def test_preflight_accepts_a_server_that_answers_initialize( + tmp_path: pathlib.Path, +) -> None: + """A stdio server that replies to ``initialize`` passes preflight.""" + server = tmp_path / "server.py" + server.write_text( + "import json, sys\n" + "line = sys.stdin.readline()\n" + "req = json.loads(line)\n" + 'print(json.dumps({"jsonrpc": "2.0", "id": req["id"], "result": {}}))\n', + encoding="utf-8", + ) + spec = mcp_swap.McpServerSpec(command=sys.executable, args=[str(server)]) + + assert mcp_swap.preflight_spec(spec, timeout=60) is None + + +def test_preflight_reports_stderr_when_the_server_never_answers( + tmp_path: pathlib.Path, +) -> None: + """A server that dies is reported with the tail of its stderr.""" + server = tmp_path / "server.py" + server.write_text( + 'import sys\nsys.stderr.write("could not resolve ref\\n")\nsys.exit(1)\n', + encoding="utf-8", + ) + spec = mcp_swap.McpServerSpec(command=sys.executable, args=[str(server)]) + + assert mcp_swap.preflight_spec(spec, timeout=60) == "could not resolve ref" + + +def test_preflight_reports_a_command_that_cannot_launch() -> None: + """A missing binary is named rather than raising.""" + spec = mcp_swap.McpServerSpec(command="mcp-swap-no-such-binary", args=[]) + + failure = mcp_swap.preflight_spec(spec, timeout=60) + + assert failure is not None + assert "mcp-swap-no-such-binary" in failure + + +def test_preflight_passes_spec_env_to_the_process(tmp_path: pathlib.Path) -> None: + """``spec.env`` reaches the launched server. + + The cooldown bypass a prerelease branch needs travels this way, so a + preflight that dropped it would reject a spec that works in an agent. + """ + server = tmp_path / "server.py" + server.write_text( + "import json, os, sys\n" + "req = json.loads(sys.stdin.readline())\n" + 'if os.environ.get("MCP_SWAP_PROBE") != "1":\n' + " sys.exit(2)\n" + 'print(json.dumps({"jsonrpc": "2.0", "id": req["id"], "result": {}}))\n', + encoding="utf-8", + ) + spec = mcp_swap.McpServerSpec( + command=sys.executable, args=[str(server)], env={"MCP_SWAP_PROBE": "1"} + ) + + assert mcp_swap.preflight_spec(spec, timeout=60) is None From bb6c1ddc7742c5218a2fa779155482f41ff76b3a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 08:51:24 -0500 Subject: [PATCH 02/71] mcp(fix[mcp_swap]): Rewrite only the entry it was asked to change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: The JSON writer re-serialized the whole document to change one entry, so it escaped every non-ASCII character in the file and appended a trailing newline the file may never have had. In ~/.claude.json that reached model labels and prompt history the swap never read, turning a one-entry edit into a diff spanning the file — noise a reviewer has to read past in `--dry-run`, and a rewrite of bytes that were not ours to touch. Dropping the escaping alone would trade one defect for a worse one: a lone surrogate, which is what a JavaScript writer emits for a string sliced through a surrogate pair, has no UTF-8 encoding, and the resulting UnicodeEncodeError is not the RuntimeError the per-CLI handler catches — it would abort the whole run. what: - Write non-ASCII literally, falling back to an escaped document for the one input that cannot be encoded - Carry the source file's trailing-newline convention across the rewrite, requiring the original bytes rather than defaulting them - Assert an unmodified config round-trips byte-identical across the shapes the agent CLIs write --- CHANGES | 9 ++ scripts/mcp_swap.py | 44 ++++++++-- tests/test_mcp_swap.py | 182 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index 2a988e60..04cc7bbd 100644 --- a/CHANGES +++ b/CHANGES @@ -26,6 +26,15 @@ round trip against the resolved command first, so a ref that does not exist — or a dependency that cannot resolve — fails before it reaches any config rather than surfacing as an opaque startup error inside every agent. +**`mcp_swap` rewrites only the entry it was asked to change** + +Swapping one MCP entry in a JSON config also re-encoded every non-ASCII +character in the file as a `\uXXXX` escape and appended a trailing newline the +file may never have had. In `~/.claude.json` that reached model labels and +prompt history the swap never read, turning a one-entry edit into a diff +spanning the file. A config the swap does not modify now comes back +byte-identical, asserted across the shapes the agent CLIs write. + #### CI actions updated to current majors Workflow actions moved to their current major releases: `actions/checkout` v7, diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 2a77f06b..a1cb81f0 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -335,11 +335,43 @@ def load_config(info: CLIInfo) -> t.Any: return tomlkit.parse(raw.decode()) -def dump_config_bytes(info: CLIInfo, config: t.Any) -> bytes: - """Serialize an edited config back to bytes in its original format.""" - if info.fmt == "json": - return (json.dumps(config, indent=2) + "\n").encode() - return tomlkit.dumps(config).encode() +def _json_trailer(original: bytes) -> str: + """Return the newline a rewritten JSON config should end with. + + Claude writes ``~/.claude.json`` without a trailing newline, so + appending one unconditionally grows the file by a byte on every swap + and shows as a diff hunk in a region the swap never touched. Empty + bytes mean a file being seeded, which gets the conventional newline. + """ + if not original: + return "\n" + return "\n" if original.endswith(b"\n") else "" + + +def dump_config_bytes(info: CLIInfo, config: t.Any, *, original: bytes) -> bytes: + """Serialize an edited config back to bytes in its original format. + + ``original`` is the file's pre-edit bytes, or empty when seeding a + new one. The parsed structure does not record the byte-level + conventions of the file it came from, so they are carried over from + the source instead. Required rather than defaulted: a caller that + omitted it would silently start rewriting regions it never touched, + which is the defect this parameter exists to prevent. tomlkit + preserves those conventions itself; only the JSON writer needs it. + """ + if info.fmt != "json": + return tomlkit.dumps(config).encode() + trailer = _json_trailer(original) + # ensure_ascii would re-escape every non-ASCII character in the file, + # including config text the swap never read. + text = json.dumps(config, indent=2, ensure_ascii=False) + trailer + try: + return text.encode() + except UnicodeEncodeError: + # A lone surrogate — a JS writer slicing a string mid-pair — has no + # UTF-8 encoding. Escaping the document is then the only form that + # can be written at all. + return (json.dumps(config, indent=2) + trailer).encode() def atomic_write(path: pathlib.Path, data: bytes) -> None: @@ -1137,7 +1169,7 @@ def cmd_use_local(args: argparse.Namespace) -> int: else spec ) action = set_server(cli, config, server, cli_spec, repo, scope=scope) - new_bytes = dump_config_bytes(info, config) + new_bytes = dump_config_bytes(info, config, original=original_bytes) except RuntimeError as exc: print(f"[{label}] {exc}", file=sys.stderr) had_error = 1 diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 2532e609..8b1d2a34 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -2143,3 +2143,185 @@ def test_preflight_passes_spec_env_to_the_process(tmp_path: pathlib.Path) -> Non ) assert mcp_swap.preflight_spec(spec, timeout=60) is None + + +# --------------------------------------------------------------------------- +# JSON writer fidelity +# +# The swap edits one entry inside a file the user owns, so bytes it did +# not set out to change must survive the rewrite. ``load_config`` -> +# ``dump_config_bytes`` is the whole write path, so an unmodified config +# has to come back byte-identical. +# +# Out of scope, and normalized rather than preserved: indent width, CRLF, +# `\/` and `\uXXXX` escapes of characters that need none, duplicate keys, +# and number spelling (`1e5` -> `100000.0`). None appear in what the six +# CLIs write — they all emit `JSON.stringify(x, null, 2)` — and none +# change what a CLI reads, only the bytes a dotfile diff shows. +# --------------------------------------------------------------------------- + + +class JSONFidelityCase(t.NamedTuple): + """A JSON config body whose exact bytes survive a no-op rewrite. + + Attributes + ---------- + test_id : str + Identifier shown in the parametrized test name. + body : str + The config file's text, written to disk verbatim. + """ + + test_id: str + body: str + + +PRESERVED_JSON: list[JSONFidelityCase] = [ + JSONFidelityCase( + "mcp_servers_block", + '{\n "mcpServers": {\n "libtmux": {\n "command": "uvx",\n' + ' "args": [\n "libtmux-mcp==0.1.0a2"\n ]\n }\n }\n}\n', + ), + JSONFidelityCase( + "non_ascii_model_label", + '{\n "model": "Fable 5 · Most capable…",\n "mcpServers": {}\n}\n', + ), + JSONFidelityCase( + "emoji_and_cjk", '{\n "history": [\n "🙂 日本語 café"\n ]\n}\n' + ), + JSONFidelityCase("escaped_lone_surrogate", '{\n "truncated": "\\ud800"\n}\n'), + JSONFidelityCase("unsorted_keys", '{\n "zeta": 1,\n "alpha": 2\n}\n'), + JSONFidelityCase( + "claude_shape_without_trailing_newline", + '{\n "model": "Fable 5 · Most capable…",\n "projects": {\n' + ' "/home/someone/repo": {\n "mcpServers": {}\n }\n }\n}', + ), +] + + +def _json_config(tmp_path: pathlib.Path, body: str) -> tuple[t.Any, bytes]: + """Write ``body`` verbatim and return its ``CLIInfo`` and exact bytes.""" + path = tmp_path / "config.json" + raw = body.encode() + path.write_bytes(raw) + info = mcp_swap.CLIInfo( + name="cursor", binary="cursor-agent", config_path=path, fmt="json" + ) + return info, raw + + +@pytest.mark.parametrize( + JSONFidelityCase._fields, + PRESERVED_JSON, + ids=[c.test_id for c in PRESERVED_JSON], +) +def test_untouched_json_config_round_trips_byte_identical( + tmp_path: pathlib.Path, test_id: str, body: str +) -> None: + """Parsing a config and writing it back unmodified changes nothing. + + Every case is a shape the JavaScript agent CLIs actually emit: + two-space indent, literal non-ASCII, escapes only below ``0x20`` plus + lone surrogates, and no terminating newline. + """ + assert test_id + info, raw = _json_config(tmp_path, body) + + assert ( + mcp_swap.dump_config_bytes(info, mcp_swap.load_config(info), original=raw) + == raw + ) + + +def test_dump_config_bytes_ends_a_seeded_file_with_a_newline( + tmp_path: pathlib.Path, +) -> None: + """With no original to match, a JSON config gets the conventional newline.""" + info, _ = _json_config(tmp_path, "") + + assert ( + mcp_swap.dump_config_bytes(info, {"mcpServers": {}}, original=b"") + == b'{\n "mcpServers": {}\n}\n' + ) + + +def test_dump_config_bytes_escapes_a_config_it_cannot_encode( + tmp_path: pathlib.Path, +) -> None: + r"""A lone surrogate has no UTF-8 form, so the document is escaped instead. + + JavaScript writes a string sliced through a surrogate pair as + ``"\ud800"``, which parses to a Python string ``str.encode`` rejects. + Escaping the whole document is what keeps the file writable at all. + """ + config = {"truncated": "\ud800", "label": "café"} + + with pytest.raises(UnicodeEncodeError): + json.dumps(config, indent=2, ensure_ascii=False).encode() + + info, _ = _json_config(tmp_path, "") + written = mcp_swap.dump_config_bytes(info, config, original=b"") + + assert written == b'{\n "truncated": "\\ud800",\n "label": "caf\\u00e9"\n}\n' + assert json.loads(written.decode()) == config + + +def test_swap_leaves_non_ascii_elsewhere_in_the_config_alone( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """A real swap does not re-escape config text it never read. + + Claude stores model labels and prompt history alongside the MCP + entries, so escaping on write turns a one-entry edit into a diff + spanning the file. + """ + info = mcp_swap.CLIS["claude"] + label = "Fable 5 · Most capable…" + _write_json( + info.config_path, + { + "model": label, + "projects": { + str(fake_repo.resolve()): { + "mcpServers": {"libtmux": _pinned_claude_entry()}, + "history": ["café ☕"], + } + }, + }, + ) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "claude"] + ) + + assert mcp_swap.cmd_use_local(args) == 0 + + after = info.config_path.read_text() + assert f'"model": "{label}"' in after + assert '"café ☕"' in after + assert "\\u" not in after + + +def test_swap_does_not_append_a_newline_the_cli_never_wrote( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """Claude's config has no trailing newline, and swapping must not add one.""" + info = mcp_swap.CLIS["claude"] + body = json.dumps( + { + "projects": { + str(fake_repo.resolve()): { + "mcpServers": {"libtmux": _pinned_claude_entry()} + } + } + }, + indent=2, + ) + info.config_path.parent.mkdir(parents=True, exist_ok=True) + info.config_path.write_text(body) + + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "claude"] + ) + assert mcp_swap.cmd_use_local(args) == 0 + + assert not info.config_path.read_bytes().endswith(b"\n") From 6597bd074272565f18d8af64210c059d41cf990f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 09:51:39 -0500 Subject: [PATCH 03/71] mcp(docs[mcp_swap]): Name the pull-request mode up front why: The module docstring described use-local as rewriting configs to run a local checkout, which is now only half of what it does. A reader meeting the file for the first time would not learn --pr exists. what: - Name the pull-request form alongside the checkout form - Add it to the examples block --- scripts/mcp_swap.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index a1cb81f0..de63c21e 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -8,7 +8,8 @@ Use when you want every installed agent CLI to run a local checkout of an MCP server (editable) instead of a pinned release. ``use-local`` rewrites each CLI's config to invoke the checkout via ``uv --directory run -``; ``revert`` restores from the timestamped backup the swap wrote. +``, or a pull request's head via ``uvx`` with ``--pr``; ``revert`` +restores from the timestamped backup the swap wrote. Swapping a layer that is already swapped keeps that first backup rather than taking a new one, so ``revert`` always lands on the pre-swap config. @@ -25,6 +26,7 @@ $ uv run scripts/mcp_swap.py status $ uv run scripts/mcp_swap.py use-local --dry-run $ uv run scripts/mcp_swap.py use-local +$ uv run scripts/mcp_swap.py use-local --pr 115 $ uv run scripts/mcp_swap.py revert ``` From 77c011aa9ad130ca77facce7a663a431d02b02f1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 09:54:10 -0500 Subject: [PATCH 04/71] mcp(docs[mcp_swap]): Say which target use-local rewrites to why: The summary line named the repo's checkout as the only outcome, so it read as false for the branch immediately below it. what: - State both targets, and which flag selects the second --- scripts/mcp_swap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index de63c21e..7e36fa56 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -1079,7 +1079,10 @@ def _points_at( def cmd_use_local(args: argparse.Namespace) -> int: - """Rewrite each target CLI's config to run the repo's checkout via ``uv``. + """Rewrite each target CLI's config to run the repo, or a pull request. + + Without ``--pr`` the entry runs the repo's checkout via ``uv``; with + it, the pull request's head via ``uvx``. The optional ``--scope`` flag selects Claude's user-level fallback vs. per-project override; see :data:`Scope`. The flag is silently From d243b5475e8e78cd159ab7faf64ab34cc631124b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 09:57:12 -0500 Subject: [PATCH 05/71] mcp(docs[mcp_swap]): Mention pull requests in use-local's help why: The subcommand list is where someone discovers what use-local is for, and it named only the checkout. what: - Name the pull-request target in the subparser help line --- scripts/mcp_swap.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 7e36fa56..8213db50 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -1572,7 +1572,9 @@ def build_parser() -> argparse.ArgumentParser: ) ps.set_defaults(func=cmd_status) - pu = sub.add_parser("use-local", help="rewrite configs to run this checkout") + pu = sub.add_parser( + "use-local", help="rewrite configs to run this checkout, or a pull request" + ) pu.add_argument("--repo", default=".", help="repo root (default: .)") pu.add_argument( "--pr", From 55ab368a46fdf9330dc0e573e7f79188a9720619 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 10:01:07 -0500 Subject: [PATCH 06/71] mcp(docs[tests]): Scope the fidelity note to the JSON CLIs why: The note claimed all six CLIs emit JSON.stringify output. Two of them, codex and grok, are TOML and never reach this writer at all. what: - Say JSON CLIs, which is the set the note is about --- tests/test_mcp_swap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 8b1d2a34..cd8d79dc 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -2155,7 +2155,7 @@ def test_preflight_passes_spec_env_to_the_process(tmp_path: pathlib.Path) -> Non # # Out of scope, and normalized rather than preserved: indent width, CRLF, # `\/` and `\uXXXX` escapes of characters that need none, duplicate keys, -# and number spelling (`1e5` -> `100000.0`). None appear in what the six +# and number spelling (`1e5` -> `100000.0`). None appear in what the JSON # CLIs write — they all emit `JSON.stringify(x, null, 2)` — and none # change what a CLI reads, only the bytes a dotfile diff shows. # --------------------------------------------------------------------------- From b4e16b634b0e9ebc1af8a47d8215007122f42404 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 10:07:51 -0500 Subject: [PATCH 07/71] mcp(docs[CHANGES]): mcp_swap pull-request targeting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: The branch carried two entries whose prose explained mechanism — worktree pruning, the initialize round trip, escape encoding — none of which a reader needs to decide whether the change matters to them. what: - Collapse them into one entry naming what the tool can now do and what it no longer does to a config --- CHANGES | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/CHANGES b/CHANGES index 04cc7bbd..f7f2f536 100644 --- a/CHANGES +++ b/CHANGES @@ -16,24 +16,12 @@ or as a bare name carrying only its type. ### Development -**`mcp_swap` can point every agent CLI at a pull request** - -`use-local --pr N` rewrites each CLI's entry to run the pull request's head -through `uvx` instead of a working copy. Nothing is checked out, so reverting -is the ordinary config restore with no worktree to prune, and a pull request -from a fork needs no special handling. The swap completes an MCP `initialize` -round trip against the resolved command first, so a ref that does not exist — -or a dependency that cannot resolve — fails before it reaches any config -rather than surfacing as an opaque startup error inside every agent. - -**`mcp_swap` rewrites only the entry it was asked to change** - -Swapping one MCP entry in a JSON config also re-encoded every non-ASCII -character in the file as a `\uXXXX` escape and appended a trailing newline the -file may never have had. In `~/.claude.json` that reached model labels and -prompt history the swap never read, turning a one-entry edit into a diff -spanning the file. A config the swap does not modify now comes back -byte-identical, asserted across the shapes the agent CLIs write. +**`mcp_swap` can target a pull request** + +`use-local --pr N` points every agent CLI at a pull request's head, so +reviewing a branch across your agents needs no checkout and leaves nothing to +clean up afterwards. Swapping a config no longer rewrites text it was not +asked to change. (#115) #### CI actions updated to current majors From 53585d25af8b3088f95417b6bbb348fae1ff43e7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 11:01:56 -0500 Subject: [PATCH 08/71] mcp(fix[mcp_swap]): Survive an unreadable config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: The per-CLI handler caught only RuntimeError, so a config that would not parse escaped as a traceback and took the whole run with it — the other CLIs never got their swap. The comment above it already claimed a clean per-CLI error, and doctor already caught the wider set. what: - Catch ValueError and OSError alongside RuntimeError in status and use-local, matching what doctor already does - Cover malformed JSON, a truncated document, and invalid UTF-8, and that one bad config does not stop the CLIs behind it --- scripts/mcp_swap.py | 16 +++++---- tests/test_mcp_swap.py | 74 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 8213db50..fa4994fd 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -1045,7 +1045,7 @@ def cmd_status(args: argparse.Namespace) -> int: print( f"[{cli}] {server} = {spec.command} {' '.join(spec.args)} ({tag})" ) - except RuntimeError as exc: + except (RuntimeError, ValueError, OSError) as exc: print(f"[{cli}] {exc}", file=sys.stderr) continue return 0 @@ -1144,11 +1144,13 @@ def cmd_use_local(args: argparse.Namespace) -> int: if not info.config_path.exists(): print(f"[{label}] skip — config not found at {info.config_path}") continue - # Wrap the read + shape-guarded mutation in try/except RuntimeError - # so a malformed Claude config (top-level mcpServers / projects not a - # mapping) surfaces as a clean per-CLI error instead of an uncaught - # traceback. Same per-CLI continuation pattern the inner write-failure - # handler below uses. + # Wrap the read + shape-guarded mutation so an unreadable config + # surfaces as a clean per-CLI error instead of an uncaught traceback. + # The three arms are the three ways it fails: a shape this script + # rejects raises RuntimeError, an unparseable one raises ValueError + # (JSON, TOML and UTF-8 decode errors all derive from it), and an + # unopenable one raises OSError. Same trio ``doctor`` catches, and + # the same per-CLI continuation the write-failure handler below uses. try: original_bytes = info.config_path.read_bytes() config = load_config(info) @@ -1175,7 +1177,7 @@ def cmd_use_local(args: argparse.Namespace) -> int: ) action = set_server(cli, config, server, cli_spec, repo, scope=scope) new_bytes = dump_config_bytes(info, config, original=original_bytes) - except RuntimeError as exc: + except (RuntimeError, ValueError, OSError) as exc: print(f"[{label}] {exc}", file=sys.stderr) had_error = 1 continue diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index cd8d79dc..bfbf4084 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -2325,3 +2325,77 @@ def test_swap_does_not_append_a_newline_the_cli_never_wrote( assert mcp_swap.cmd_use_local(args) == 0 assert not info.config_path.read_bytes().endswith(b"\n") + + +class UnreadableConfigCase(t.NamedTuple): + """A config body that cannot be parsed, and the error it provokes. + + Attributes + ---------- + test_id : str + Identifier shown in the parametrized test name. + body : bytes + Exact bytes written to the config file. + """ + + test_id: str + body: bytes + + +UNREADABLE_CONFIGS: list[UnreadableConfigCase] = [ + UnreadableConfigCase("malformed_json", b"{ this is not json"), + UnreadableConfigCase("truncated_json", b'{"mcpServers": {'), + UnreadableConfigCase("invalid_utf8", b'{"a": "\xff\xfe"}'), +] + + +@pytest.mark.parametrize( + UnreadableConfigCase._fields, + UNREADABLE_CONFIGS, + ids=[c.test_id for c in UNREADABLE_CONFIGS], +) +def test_unreadable_config_reports_instead_of_crashing( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, + capsys: pytest.CaptureFixture[str], + test_id: str, + body: bytes, +) -> None: + """A config that will not parse is reported and skipped, not raised through. + + ``load_config`` raises ``ValueError`` for every unparseable form — + JSON, TOML and UTF-8 decode errors all derive from it — which the + per-CLI handler has to catch for the run to survive one bad file. + """ + assert test_id + info = mcp_swap.CLIS["cursor"] + info.config_path.parent.mkdir(parents=True, exist_ok=True) + info.config_path.write_bytes(body) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + + assert mcp_swap.cmd_use_local(args) == 1 + + assert "cursor" in capsys.readouterr().err + assert info.config_path.read_bytes() == body + + +def test_unreadable_config_does_not_stop_the_other_clis( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, +) -> None: + """One bad config does not prevent the remaining CLIs from swapping.""" + bad = mcp_swap.CLIS["cursor"] + bad.config_path.parent.mkdir(parents=True, exist_ok=True) + bad.config_path.write_bytes(b"{ not json") + good = mcp_swap.CLIS["gemini"] + _write_json(good.config_path, {"mcpServers": {}}) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor", "--cli", "gemini"] + ) + + assert mcp_swap.cmd_use_local(args) == 1 + + written = json.loads(good.config_path.read_text()) + assert "libtmux" in written["mcpServers"] From 44b749d0c80fec264d46964814cd8f939da6f6f6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 11:28:36 -0500 Subject: [PATCH 09/71] mcp(fix[mcp_swap]): Survive an unreadable swap state file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: load_state parsed the file with a bare json.loads, so a truncated or hand-edited one raised through every command that reads it — revert and doctor included. Its own docstring already promised a hand-edited file could not crash the script. Returning empty silently would be its own trap: it means the record of every swap is gone, so revert would report nothing to unwind while swapped configs and their backups sit on disk. Naming the file is what lets someone go find those backups. what: - Degrade to no entries when the file will not parse, or holds a shape that carries none, and say so on stderr --- scripts/mcp_swap.py | 16 +++++++-- tests/test_mcp_swap.py | 81 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index fa4994fd..65df14d0 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -885,11 +885,23 @@ def load_state() -> dict[tuple[CLIName, Scope], SwapEntry]: (those that don't parse as ``cli:scope``) and entries with a non-coercible ``seq_no`` or missing required fields are dropped silently so a hand-edited file cannot crash the script. + + A file that will not parse at all is reported rather than dropped + silently: it means the record of every swap is gone, so ``revert`` + is about to say there is nothing to unwind while swapped configs + and their backups sit on disk. Saying so is what lets the operator + go find those backups. """ if not STATE_FILE.exists(): return {} - raw = json.loads(STATE_FILE.read_text()) - entries = raw.get("entries", {}) + try: + raw = json.loads(STATE_FILE.read_text()) + except (OSError, ValueError) as exc: + print(f"swap state unreadable ({STATE_FILE}): {exc}", file=sys.stderr) + return {} + entries = raw.get("entries", {}) if isinstance(raw, dict) else {} + if not isinstance(entries, dict): + entries = {} out: dict[tuple[CLIName, Scope], SwapEntry] = {} for k, v in entries.items(): parsed = _parse_state_key(k) diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index bfbf4084..4de2bff9 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -2399,3 +2399,84 @@ def test_unreadable_config_does_not_stop_the_other_clis( written = json.loads(good.config_path.read_text()) assert "libtmux" in written["mcpServers"] + + +class CorruptStateCase(t.NamedTuple): + """A swap-state file body that cannot yield entries. + + Attributes + ---------- + test_id : str + Identifier shown in the parametrized test name. + body : str + Exact text written to the state file. + """ + + test_id: str + body: str + + +CORRUPT_STATE: list[CorruptStateCase] = [ + CorruptStateCase("not_json", "{ not json at all"), + CorruptStateCase("empty_file", ""), + CorruptStateCase("json_but_a_list", "[1, 2, 3]"), + CorruptStateCase("entries_not_a_mapping", '{"entries": "nope"}'), +] + + +@pytest.mark.parametrize( + CorruptStateCase._fields, + CORRUPT_STATE, + ids=[c.test_id for c in CORRUPT_STATE], +) +def test_corrupt_swap_state_is_reported_not_raised( + fake_home: pathlib.Path, + test_id: str, + body: str, +) -> None: + """A state file that yields no entries degrades to empty, never raises. + + ``revert`` and ``doctor`` both read this file before doing anything, + so a hand-edited or truncated one would otherwise take down every + command that consults it. + """ + assert test_id + mcp_swap.STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + mcp_swap.STATE_FILE.write_text(body, encoding="utf-8") + + assert mcp_swap.load_state() == {} + + +def test_unparseable_swap_state_names_the_file( + fake_home: pathlib.Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """An unreadable state file says so, because backups are now orphaned. + + Returning empty silently would let ``revert`` report nothing to do + while swapped configs and their backups sit on disk. + """ + mcp_swap.STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + mcp_swap.STATE_FILE.write_text("{ not json", encoding="utf-8") + + mcp_swap.load_state() + + assert str(mcp_swap.STATE_FILE) in capsys.readouterr().err + + +def test_revert_survives_a_corrupt_state_file( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, +) -> None: + """``revert`` reports nothing to unwind rather than crashing.""" + info = mcp_swap.CLIS["cursor"] + _write_json(info.config_path, {"mcpServers": {}}) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + assert mcp_swap.cmd_use_local(args) == 0 + mcp_swap.STATE_FILE.write_text("{ corrupted", encoding="utf-8") + + revert_args = mcp_swap.build_parser().parse_args(["revert", "--cli", "cursor"]) + + assert mcp_swap.cmd_revert(revert_args) in (0, 1) From cd7e9c2895521ca31eaa3e3c8445ae01c0aa98b1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 11:34:33 -0500 Subject: [PATCH 10/71] mcp(fix[mcp_swap]): Report a backup that cannot be written why: The backup write sat between the two guarded blocks, so an unwritable config directory raised a PermissionError through the whole run and the CLIs behind it never got their swap. Aborting that CLI is the right half of the trade rather than swapping anyway: the backup is the only copy of the pre-swap config, so a swap that could not take one would leave nothing to revert to. what: - Catch the failure, name it per CLI, and move on to the next --- scripts/mcp_swap.py | 18 +++++++++++--- tests/test_mcp_swap.py | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 65df14d0..f6245bd1 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -1234,10 +1234,20 @@ def cmd_use_local(args: argparse.Namespace) -> int: backup_suffix = f"{BACKUP_SUFFIX_PREFIX}{ts}" if cli == "claude": backup_suffix += f"-{scope}" - backup_path = write_new_backup( - info.config_path.with_suffix(info.config_path.suffix + backup_suffix), - original_bytes, - ) + # A backup that cannot be written must abort this CLI rather + # than degrade into a swap with nothing to revert to — an + # unwritable directory is the case that produces both. + try: + backup_path = write_new_backup( + info.config_path.with_suffix( + info.config_path.suffix + backup_suffix + ), + original_bytes, + ) + except OSError as exc: + print(f"[{label}] cannot write backup: {exc}", file=sys.stderr) + had_error = 1 + continue backup_note = f"backup: {backup_path}" try: atomic_write(info.config_path, new_bytes) diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 4de2bff9..721e3fe8 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -9,6 +9,7 @@ import importlib.util import json +import os import pathlib import sys import types @@ -2480,3 +2481,58 @@ def test_revert_survives_a_corrupt_state_file( revert_args = mcp_swap.build_parser().parse_args(["revert", "--cli", "cursor"]) assert mcp_swap.cmd_revert(revert_args) in (0, 1) + + +def test_unwritable_directory_aborts_before_swapping( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """A config whose backup cannot be written is left alone. + + The backup is the only copy of the pre-swap config, so a swap that + could not take one would leave nothing to revert to. Aborting the + CLI is the safe half of that trade. + """ + if os.geteuid() == 0: + pytest.skip("root ignores directory permissions") + info = mcp_swap.CLIS["grok"] + info.config_path.parent.mkdir(parents=True, exist_ok=True) + original = '[mcp_servers.other]\ncommand = "x"\n' + info.config_path.write_text(original, encoding="utf-8") + info.config_path.parent.chmod(0o500) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "grok"] + ) + + try: + assert mcp_swap.cmd_use_local(args) == 1 + assert "backup" in capsys.readouterr().err + assert info.config_path.read_text() == original + finally: + info.config_path.parent.chmod(0o700) + + +def test_unwritable_directory_does_not_stop_the_other_clis( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, +) -> None: + """One unwritable config directory does not abort the whole run.""" + if os.geteuid() == 0: + pytest.skip("root ignores directory permissions") + blocked = mcp_swap.CLIS["grok"] + blocked.config_path.parent.mkdir(parents=True, exist_ok=True) + blocked.config_path.write_text('[mcp_servers.o]\ncommand = "x"\n', encoding="utf-8") + blocked.config_path.parent.chmod(0o500) + reachable = mcp_swap.CLIS["cursor"] + _write_json(reachable.config_path, {"mcpServers": {}}) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "grok", "--cli", "cursor"] + ) + + try: + assert mcp_swap.cmd_use_local(args) == 1 + written = json.loads(reachable.config_path.read_text()) + assert "libtmux" in written["mcpServers"] + finally: + blocked.config_path.parent.chmod(0o700) From 572dc5ba6cbb389932c6faeed94d559a2fcce915 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 11:39:50 -0500 Subject: [PATCH 11/71] mcp(fix[mcp_swap]): Reject a --pr value that is not a pull request why: --pr took any int, so a typo built a ref like refs/pull/-5/head and carried it as far as the preflight. Pull requests are numbered from one, so a non-positive value can only be a mistake. what: - Parse --pr through a validator that requires a positive number, matching how --env already reports a malformed argument --- scripts/mcp_swap.py | 15 ++++++++++++++- tests/test_mcp_swap.py | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index f6245bd1..90a7ee91 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -1388,6 +1388,19 @@ def _env_pair(raw: str) -> tuple[str, str]: return key, value +def _pr_number(raw: str) -> int: + """Parse a ``--pr`` argument as a pull-request number, or raise for argparse.""" + try: + number = int(raw) + except ValueError: + msg = f"--pr expects a number, got {raw!r}" + raise argparse.ArgumentTypeError(msg) from None + if number < 1: + msg = f"--pr expects a positive number, got {number}" + raise argparse.ArgumentTypeError(msg) + return number + + def _config_present_clis() -> list[CLIName]: """CLIs whose config file exists — enough to *read* entries (no binary needed). @@ -1602,7 +1615,7 @@ def build_parser() -> argparse.ArgumentParser: pu.add_argument("--repo", default=".", help="repo root (default: .)") pu.add_argument( "--pr", - type=int, + type=_pr_number, metavar="N", help=( "Point the CLIs at pull request N instead of the working copy. " diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 721e3fe8..591b86ea 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -7,6 +7,7 @@ from __future__ import annotations +import argparse import importlib.util import json import os @@ -2536,3 +2537,26 @@ def test_unwritable_directory_does_not_stop_the_other_clis( assert "libtmux" in written["mcpServers"] finally: blocked.config_path.parent.chmod(0o700) + + +@pytest.mark.parametrize("raw", ["0", "-5", "notanumber", "1.5", ""]) +def test_pr_number_rejects_what_is_not_a_pull_request(raw: str) -> None: + """``--pr`` takes a positive number; anything else stops at the parser. + + Pull requests are numbered from one, so a non-positive value can only + be a typo. Catching it here keeps it out of the ref the swap builds. + """ + with pytest.raises(argparse.ArgumentTypeError): + mcp_swap._pr_number(raw) + + +def test_pr_number_accepts_a_pull_request_number() -> None: + """A positive number parses to an int.""" + assert mcp_swap._pr_number("115") == 115 + + +@pytest.mark.parametrize("raw", ["0", "-5", "notanumber"]) +def test_parser_rejects_a_bad_pr_argument(raw: str) -> None: + """The parser exits rather than building a ref from a bad number.""" + with pytest.raises(SystemExit): + mcp_swap.build_parser().parse_args(["use-local", "--pr", raw]) From 82d7c3050bf382986ea08b7279285c6cadc19fd5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 12:25:26 -0500 Subject: [PATCH 12/71] mcp(fix[mcp_swap]): Preserve config symlinks why: atomic_write staged beside and replaced the config path. A config symlink into a dotfiles checkout was therefore destroyed while its target stayed stale. what: - Resolve symlinks before staging so rename stays atomic at the target - Cover link chains and swap/revert recovery with sandboxed tests --- scripts/mcp_swap.py | 18 ++++++-- tests/test_mcp_swap.py | 102 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 90a7ee91..4119f57a 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -377,14 +377,24 @@ def dump_config_bytes(info: CLIInfo, config: t.Any, *, original: bytes) -> bytes def atomic_write(path: pathlib.Path, data: bytes) -> None: - """Write bytes to ``path`` via tempfile + ``os.replace`` to avoid partial writes.""" - path.parent.mkdir(parents=True, exist_ok=True) - fd, tmp_name = tempfile.mkstemp(prefix=path.name + ".", dir=str(path.parent)) + """Write bytes to ``path`` without replacing a symlinked config. + + Parameters + ---------- + path : pathlib.Path + Destination path. A symlink resolves to its final target so the + write preserves every link in the chain. + data : bytes + Bytes to write atomically. + """ + target = path.resolve() if path.is_symlink() else path + target.parent.mkdir(parents=True, exist_ok=True) + fd, tmp_name = tempfile.mkstemp(prefix=target.name + ".", dir=str(target.parent)) tmp = pathlib.Path(tmp_name) try: with os.fdopen(fd, "wb") as fh: fh.write(data) - tmp.replace(path) + tmp.replace(target) except Exception: tmp.unlink(missing_ok=True) raise diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 591b86ea..e32dbcda 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -2560,3 +2560,105 @@ def test_parser_rejects_a_bad_pr_argument(raw: str) -> None: """The parser exits rather than building a ref from a bad number.""" with pytest.raises(SystemExit): mcp_swap.build_parser().parse_args(["use-local", "--pr", raw]) + + +# --------------------------------------------------------------------------- +# Atomic writes through symlinked configs +# --------------------------------------------------------------------------- + + +def _build_symlink_chain( + root: pathlib.Path, hops: int +) -> tuple[pathlib.Path, pathlib.Path, list[pathlib.Path]]: + """Create ``hops`` links ending at an existing config file. + + Parameters + ---------- + root : pathlib.Path + Empty directory where the link and target trees are created. + hops : int + Number of links in the chain. + + Returns + ------- + tuple of pathlib.Path, pathlib.Path, list of pathlib.Path + Entry path, final target, and each link in the chain. + """ + target = root / "dotfiles" / "mcp.json" + target.parent.mkdir(parents=True) + target.write_bytes(b"original\n") + link_dir = root / "home" + link_dir.mkdir() + links: list[pathlib.Path] = [] + entry = target + for hop in range(hops): + link = link_dir / f"hop-{hop}.json" + link.symlink_to(entry) + links.append(link) + entry = link + return entry, target, links + + +@pytest.mark.parametrize("hops", [1, 3], ids=["single", "chain"]) +def test_atomic_write_updates_the_symlink_target( + tmp_path: pathlib.Path, hops: int +) -> None: + """The final target receives the bytes and every link survives.""" + entry, target, links = _build_symlink_chain(tmp_path, hops) + + mcp_swap.atomic_write(entry, b"swapped\n") + + assert all(link.is_symlink() for link in links) + assert target.read_bytes() == b"swapped\n" + assert entry.read_bytes() == b"swapped\n" + + +def test_atomic_write_stages_beside_the_symlink_target( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The temp file shares the final target's filesystem for atomic rename.""" + entry, target, _links = _build_symlink_chain(tmp_path, 1) + real_mkstemp = mcp_swap.tempfile.mkstemp + staged_in: list[str | None] = [] + + def recording_mkstemp(*args: t.Any, **kwargs: t.Any) -> tuple[int, str]: + staged_in.append(kwargs.get("dir")) + return t.cast(tuple[int, str], real_mkstemp(*args, **kwargs)) + + monkeypatch.setattr(mcp_swap.tempfile, "mkstemp", recording_mkstemp) + + mcp_swap.atomic_write(entry, b"swapped\n") + + assert staged_in == [str(target.parent)] + + +def test_symlinked_config_swap_and_revert_round_trip( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """Swap and revert update the target without replacing the config link.""" + info = mcp_swap.CLIS["cursor"] + target = fake_home / "dotfiles" / "cursor" / "mcp.json" + _write_json(target, {"mcpServers": {"libtmux": _pinned_json_entry()}}) + original = target.read_bytes() + info.config_path.parent.mkdir(parents=True) + info.config_path.symlink_to(target) + parser = mcp_swap.build_parser() + + assert ( + mcp_swap.cmd_use_local( + parser.parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + ) + == 0 + ) + state = mcp_swap.load_state()["cursor", "user"] + backup = pathlib.Path(state.backup_path) + assert info.config_path.is_symlink() + assert backup.parent == info.config_path.parent + assert json.loads(target.read_text())["mcpServers"]["libtmux"]["command"] == "uv" + + assert mcp_swap.cmd_revert(parser.parse_args(["revert", "--cli", "cursor"])) == 0 + assert info.config_path.is_symlink() + assert target.read_bytes() == original + assert not backup.exists() From fe7dbe3e10f5d8d82cfc2f31ee22bab82f774c7d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 13:08:35 -0500 Subject: [PATCH 13/71] mcp(fix[mcp_swap]): Make recovery atomic why: Concurrent swaps and partial filesystem failures could orphan the pristine backup, lose recovery state, or restore through a repointed symlink. what: - Serialize mutations and write recovery state before config changes - Restore the original target while preserving file modes - Keep recovery material on failure and return nonzero when incomplete - Add adversarial coverage for races and filesystem failures --- scripts/mcp_swap.py | 208 +++++++++++++++++++++------- tests/test_mcp_swap.py | 300 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 452 insertions(+), 56 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 4119f57a..6139b10b 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -73,13 +73,16 @@ from __future__ import annotations import argparse +import contextlib import dataclasses import difflib +import fcntl import json import os import pathlib import re import shutil +import stat import subprocess import sys import tempfile @@ -317,6 +320,15 @@ class SwapEntry: #: ``Lib/sched.py`` uses to break ties on ``Event(time, priority, #: sequence, …)``. seq_no: int + #: Exact destination changed by the swap. ``config_path`` may be a + #: symlink that is later repointed, so it is not sufficient recovery + #: identity. Older state entries omit this field and fall back to + #: ``config_path`` during revert. + target_path: str | None = None + + +class SwapStateError(RuntimeError): + """Swap state is unsafe to use for a mutating operation.""" # --------------------------------------------------------------------------- @@ -389,10 +401,13 @@ def atomic_write(path: pathlib.Path, data: bytes) -> None: """ target = path.resolve() if path.is_symlink() else path target.parent.mkdir(parents=True, exist_ok=True) + mode = stat.S_IMODE(target.stat().st_mode) if target.exists() else None fd, tmp_name = tempfile.mkstemp(prefix=target.name + ".", dir=str(target.parent)) tmp = pathlib.Path(tmp_name) try: with os.fdopen(fd, "wb") as fh: + if mode is not None: + os.fchmod(fh.fileno(), mode) fh.write(data) tmp.replace(target) except Exception: @@ -887,7 +902,7 @@ def preflight_spec(spec: McpServerSpec, *, timeout: float = 300.0) -> str | None # --------------------------------------------------------------------------- -def load_state() -> dict[tuple[CLIName, Scope], SwapEntry]: +def load_state(*, strict: bool = False) -> dict[tuple[CLIName, Scope], SwapEntry]: """Read the swap-state file, returning an empty mapping when absent. The state file's schema is internal — no compatibility contract — @@ -900,30 +915,63 @@ def load_state() -> dict[tuple[CLIName, Scope], SwapEntry]: silently: it means the record of every swap is gone, so ``revert`` is about to say there is nothing to unwind while swapped configs and their backups sit on disk. Saying so is what lets the operator - go find those backups. + go find those backups. Mutating callers pass ``strict=True`` so an + unreadable or malformed record blocks changes instead of being + overwritten as empty state. """ if not STATE_FILE.exists(): return {} try: raw = json.loads(STATE_FILE.read_text()) except (OSError, ValueError) as exc: - print(f"swap state unreadable ({STATE_FILE}): {exc}", file=sys.stderr) + message = f"swap state unreadable ({STATE_FILE}): {exc}" + print(message, file=sys.stderr) + if strict: + raise SwapStateError(message) from exc + return {} + if not isinstance(raw, dict): + if strict: + message = f"swap state has invalid shape: {STATE_FILE}" + print(message, file=sys.stderr) + raise SwapStateError(message) return {} - entries = raw.get("entries", {}) if isinstance(raw, dict) else {} + entries = raw.get("entries", {}) if not isinstance(entries, dict): + if strict: + message = f"swap state has invalid entries: {STATE_FILE}" + print(message, file=sys.stderr) + raise SwapStateError(message) entries = {} out: dict[tuple[CLIName, Scope], SwapEntry] = {} for k, v in entries.items(): parsed = _parse_state_key(k) if parsed is None: + if strict: + message = f"swap state has invalid key {k!r}: {STATE_FILE}" + print(message, file=sys.stderr) + raise SwapStateError(message) continue entry = _parse_state_entry(v) if entry is None: + if strict: + message = f"swap state has invalid entry {k!r}: {STATE_FILE}" + print(message, file=sys.stderr) + raise SwapStateError(message) continue out[parsed] = entry return out +@contextlib.contextmanager +def _state_lock() -> t.Iterator[None]: + """Serialize config mutations that share the swap state file.""" + STATE_DIR.mkdir(parents=True, exist_ok=True) + fd = os.open(STATE_DIR / "state.lock", os.O_RDWR | os.O_CREAT, 0o600) + with os.fdopen(fd, "a+b") as lock_file: + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) + yield + + def save_state(entries: dict[tuple[CLIName, Scope], SwapEntry]) -> None: """Write the swap-state file atomically.""" STATE_DIR.mkdir(parents=True, exist_ok=True) @@ -936,13 +984,10 @@ def save_state(entries: dict[tuple[CLIName, Scope], SwapEntry]) -> None: atomic_write(STATE_FILE, (json.dumps(payload, indent=2) + "\n").encode("utf-8")) -def clear_state(keys: t.Iterable[tuple[CLIName, Scope]]) -> None: - """Remove the given ``(cli, scope)`` keys; delete the file if empty.""" - current = load_state() - for key in keys: - current.pop(key, None) - if current: - save_state(current) +def _save_or_clear_state(entries: dict[tuple[CLIName, Scope], SwapEntry]) -> None: + """Persist ``entries``, removing the state file when the mapping is empty.""" + if entries: + save_state(entries) elif STATE_FILE.exists(): STATE_FILE.unlink() @@ -1100,7 +1145,7 @@ def _points_at( return current.is_local_uv_directory() and current.local_repo_path() == repo -def cmd_use_local(args: argparse.Namespace) -> int: +def _cmd_use_local(args: argparse.Namespace) -> int: """Rewrite each target CLI's config to run the repo, or a pull request. Without ``--pr`` the entry runs the repo's checkout via ``uv``; with @@ -1157,7 +1202,7 @@ def cmd_use_local(args: argparse.Namespace) -> int: return 1 ts = time.strftime("%Y%m%d%H%M%S") - state = load_state() + state = load_state(strict=True) had_error = 0 for cli in targets: scope = _normalize_scope(cli, args.scope) @@ -1165,7 +1210,10 @@ def cmd_use_local(args: argparse.Namespace) -> int: info = CLIS[cli] if not info.config_path.exists(): print(f"[{label}] skip — config not found at {info.config_path}") + had_error = 1 continue + target_path = info.config_path.resolve() + target_info = dataclasses.replace(info, config_path=target_path) # Wrap the read + shape-guarded mutation so an unreadable config # surfaces as a clean per-CLI error instead of an uncaught traceback. # The three arms are the three ways it fails: a shape this script @@ -1174,8 +1222,8 @@ def cmd_use_local(args: argparse.Namespace) -> int: # unopenable one raises OSError. Same trio ``doctor`` catches, and # the same per-CLI continuation the write-failure handler below uses. try: - original_bytes = info.config_path.read_bytes() - config = load_config(info) + original_bytes = target_path.read_bytes() + config = load_config(target_info) current = get_server(cli, config, server, repo, scope=scope) if ( current @@ -1259,17 +1307,6 @@ def cmd_use_local(args: argparse.Namespace) -> int: had_error = 1 continue backup_note = f"backup: {backup_path}" - try: - atomic_write(info.config_path, new_bytes) - _revalidate(info) - except Exception as exc: - atomic_write(info.config_path, original_bytes) - print( - f"[{label}] write failed ({exc}); backup at {backup_path}", - file=sys.stderr, - ) - had_error = 1 - continue if prior is not None and backup_path == prior_backup: # ``swapped_at`` mirrors the timestamp in the backup filename # and ``seq_no`` fixes the backup's place in the unwind @@ -1278,27 +1315,76 @@ def cmd_use_local(args: argparse.Namespace) -> int: else: seq_no = max((e.seq_no for e in state.values()), default=-1) + 1 swapped_at = ts - state[(cli, scope)] = SwapEntry( + next_state = dict(state) + next_state[(cli, scope)] = SwapEntry( config_path=str(info.config_path), backup_path=str(backup_path), server=server, action=action, swapped_at=swapped_at, seq_no=seq_no, + target_path=str(target_path), ) + try: + save_state(next_state) + except OSError as exc: + print( + f"[{label}] cannot save recovery state ({exc}); config unchanged; " + f"backup at {backup_path}", + file=sys.stderr, + ) + had_error = 1 + continue + previous_state = state + state = next_state + try: + atomic_write(target_path, new_bytes) + _revalidate(target_info) + except Exception as exc: + try: + atomic_write(target_path, original_bytes) + except Exception as rollback_exc: + rollback_note = f"; rollback failed ({rollback_exc})" + else: + rollback_note = "; original config restored" + try: + _save_or_clear_state(previous_state) + except OSError as state_exc: + rollback_note += f"; recovery state cleanup failed ({state_exc})" + else: + state = previous_state + print( + f"[{label}] write failed ({exc}){rollback_note}; " + f"backup at {backup_path}", + file=sys.stderr, + ) + had_error = 1 + continue print(f"[{label}] {action}; {backup_note}") - if not args.dry_run: - save_state(state) return had_error +def cmd_use_local(args: argparse.Namespace) -> int: + """Run :func:`_cmd_use_local` under the shared mutation lock.""" + if args.dry_run: + return _cmd_use_local(args) + try: + with _state_lock(): + return _cmd_use_local(args) + except SwapStateError: + return 1 + except OSError as exc: + print(f"swap state unavailable: {exc}", file=sys.stderr) + return 1 + + def _revalidate(info: CLIInfo) -> None: """Re-parse the file after writing; raise on failure.""" load_config(info) -def cmd_revert(args: argparse.Namespace) -> int: +def _cmd_revert(args: argparse.Namespace) -> int: """Restore each target CLI's config from the backup recorded in the state file. Without ``--scope``, every recorded entry for the targeted CLIs is @@ -1307,14 +1393,14 @@ def cmd_revert(args: argparse.Namespace) -> int: the matching scope is reverted; the parameter is silently coerced to ``"user"`` for non-Claude CLIs. """ - state = load_state() + state = load_state(strict=True) # Without --cli, revert every CLI that has any recorded swap. targets = list(args.cli) if args.cli else list({cli for cli, _scope in state}) if not targets: print("no recorded swaps — nothing to revert", file=sys.stderr) return 1 - reverted: list[tuple[CLIName, Scope]] = [] + had_error = 0 for cli in targets: if args.scope is not None: wanted_scopes: tuple[Scope, ...] = (_normalize_scope(cli, args.scope),) @@ -1347,28 +1433,56 @@ def cmd_revert(args: argparse.Namespace) -> int: entry = state[key] label = f"{sc_cli}:{sc_scope}" if sc_cli == "claude" else sc_cli backup = pathlib.Path(entry.backup_path) - dest = pathlib.Path(entry.config_path) + dest = pathlib.Path(entry.target_path or entry.config_path) if not backup.exists(): print(f"[{label}] backup missing: {backup}", file=sys.stderr) - continue + had_error = 1 + break if args.dry_run: print(f"[{label}] would restore {dest} from {backup}") continue - atomic_write(dest, backup.read_bytes()) - # Backup served its purpose; LIFO unwind for this layer is - # complete. Delete on success, keep on error — same idiom - # CPython's ``tempfile.NamedTemporaryFile`` uses - # (Lib/tempfile.py:614-618). If ``atomic_write`` had raised, - # this line wouldn't run and the backup would survive for - # post-mortem; on success the backup is redundant and would - # otherwise accumulate forever across swap/revert cycles. - backup.unlink() + try: + atomic_write(dest, backup.read_bytes()) + except OSError as exc: + print(f"[{label}] restore failed: {exc}", file=sys.stderr) + had_error = 1 + break + next_state = dict(state) + next_state.pop(key) + try: + _save_or_clear_state(next_state) + except OSError as exc: + print( + f"[{label}] restored, but recovery state could not be updated: " + f"{exc}", + file=sys.stderr, + ) + had_error = 1 + break + state = next_state + try: + backup.unlink() + except OSError as exc: + print( + f"[{label}] restored; backup cleanup failed: {exc}", file=sys.stderr + ) + had_error = 1 print(f"[{label}] restored from {backup}") - reverted.append(key) + return had_error - if not args.dry_run and reverted: - clear_state(reverted) - return 0 + +def cmd_revert(args: argparse.Namespace) -> int: + """Run :func:`_cmd_revert` under the shared mutation lock.""" + if args.dry_run: + return _cmd_revert(args) + try: + with _state_lock(): + return _cmd_revert(args) + except SwapStateError: + return 1 + except OSError as exc: + print(f"swap state unavailable: {exc}", file=sys.stderr) + return 1 # --------------------------------------------------------------------------- diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index e32dbcda..5291f636 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -13,6 +13,8 @@ import os import pathlib import sys +import threading +import time import types import typing as t @@ -851,6 +853,58 @@ def test_save_state_writes_atomically(fake_home: pathlib.Path) -> None: assert leftovers == [], f"unexpected tempfile leftovers: {leftovers}" +def test_use_local_serializes_the_full_state_transaction( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Concurrent commands cannot lose one another's recovery records.""" + for cli in ("cursor", "gemini"): + _write_json( + mcp_swap.CLIS[cli].config_path, + {"mcpServers": {"libtmux": _pinned_json_entry()}}, + ) + parser = mcp_swap.build_parser() + real_save_state = mcp_swap.save_state + guard = threading.Lock() + gate = threading.Barrier(3) + active = 0 + overlapped = False + results: list[int] = [] + + def slow_save_state(entries: dict[t.Any, t.Any]) -> None: + nonlocal active, overlapped + with guard: + active += 1 + overlapped = overlapped or active > 1 + try: + time.sleep(0.1) + real_save_state(entries) + finally: + with guard: + active -= 1 + + def swap(cli: str) -> None: + args = parser.parse_args(["use-local", "--repo", str(fake_repo), "--cli", cli]) + gate.wait() + results.append(mcp_swap.cmd_use_local(args)) + + monkeypatch.setattr(mcp_swap, "save_state", slow_save_state) + threads = [ + threading.Thread(target=swap, args=(cli,)) for cli in ("cursor", "gemini") + ] + for thread in threads: + thread.start() + gate.wait() + for thread in threads: + thread.join(timeout=2) + + assert results == [0, 0] + assert not overlapped + assert set(mcp_swap.load_state()) == {("cursor", "user"), ("gemini", "user")} + assert all(not thread.is_alive() for thread in threads) + + # --------------------------------------------------------------------------- # McpServerSpec helpers # --------------------------------------------------------------------------- @@ -1144,20 +1198,20 @@ def test_load_state_drops_entries_with_missing_required_fields( assert state == {} -def test_revert_with_corrupt_seq_no_does_not_crash( +def test_revert_with_corrupt_seq_no_preserves_every_recovery_layer( fake_home: pathlib.Path, fake_repo: pathlib.Path, ) -> None: - """Same-CLI two-scope state with one corrupt ``seq_no`` does not raise TypeError. + """Same-file recovery stops when one layer has a corrupt ``seq_no``. Regression: the LIFO sort at ``cmd_revert`` would compare ``int`` vs ``str`` (``int < str`` raises in Python 3) when two same-CLI entries existed and one had a hand-edited corrupt counter. Cross-CLI buckets are length-1 and never invoke comparison — making the failure mode asymmetric, only triggering on Claude - project + user. Validating at load time eliminates the - asymmetry: the corrupt entry is dropped before it reaches the - sort, so the well-formed entry's revert applies normally. + project + user. Dropping that layer and applying the other backup + would violate LIFO order, so mutation is refused while all recovery + material remains intact. """ info = mcp_swap.CLIS["claude"] _write_json( @@ -1197,15 +1251,16 @@ def test_revert_with_corrupt_seq_no_does_not_crash( == 0 ) - # Hand-edit one of the two entries to corrupt seq_no. + before_config = info.config_path.read_bytes() raw = json.loads(mcp_swap.STATE_FILE.read_text()) raw["entries"]["claude:user"]["seq_no"] = "not-an-int" mcp_swap.STATE_FILE.write_text(json.dumps(raw)) + corrupt_state = mcp_swap.STATE_FILE.read_bytes() - # Revert must NOT raise TypeError. The corrupt entry is silently - # dropped at load time; the well-formed entry's revert applies. rc = mcp_swap.cmd_revert(parser.parse_args(["revert", "--cli", "claude"])) - assert rc == 0 + assert rc == 1 + assert info.config_path.read_bytes() == before_config + assert mcp_swap.STATE_FILE.read_bytes() == corrupt_state # --------------------------------------------------------------------------- @@ -1275,6 +1330,40 @@ def test_revert_dry_run_keeps_backup( assert backup.exists() +def test_revert_returns_failure_when_the_recorded_backup_is_missing( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """Automation receives a nonzero status when recovery cannot complete.""" + info = mcp_swap.CLIS["cursor"] + _write_json(info.config_path, {"mcpServers": {"libtmux": _pinned_json_entry()}}) + parser = mcp_swap.build_parser() + assert ( + mcp_swap.cmd_use_local( + parser.parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + ) + == 0 + ) + backup = pathlib.Path(mcp_swap.load_state()["cursor", "user"].backup_path) + backup.unlink() + + assert mcp_swap.cmd_revert(parser.parse_args(["revert", "--cli", "cursor"])) == 1 + assert ("cursor", "user") in mcp_swap.load_state() + + +def test_explicit_missing_config_returns_failure_without_creating_state( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """An explicitly requested absent config is an error, not a successful no-op.""" + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + + assert mcp_swap.cmd_use_local(args) == 1 + assert not mcp_swap.STATE_FILE.exists() + + # --------------------------------------------------------------------------- # `status --scope` filter — completes symmetry with use-local / revert. # --------------------------------------------------------------------------- @@ -2484,6 +2573,25 @@ def test_revert_survives_a_corrupt_state_file( assert mcp_swap.cmd_revert(revert_args) in (0, 1) +def test_corrupt_state_blocks_a_new_swap_without_touching_the_config( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """Unreadable recovery bookkeeping is never overwritten as empty state.""" + info = mcp_swap.CLIS["cursor"] + _write_json(info.config_path, {"mcpServers": {"libtmux": _pinned_json_entry()}}) + original = info.config_path.read_bytes() + corrupt = b"{ not json" + mcp_swap.STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + mcp_swap.STATE_FILE.write_bytes(corrupt) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + + assert mcp_swap.cmd_use_local(args) == 1 + assert info.config_path.read_bytes() == original + assert mcp_swap.STATE_FILE.read_bytes() == corrupt + + def test_unwritable_directory_aborts_before_swapping( fake_home: pathlib.Path, fake_repo: pathlib.Path, @@ -2539,6 +2647,130 @@ def test_unwritable_directory_does_not_stop_the_other_clis( blocked.config_path.parent.chmod(0o700) +def test_state_write_failure_leaves_the_config_unchanged( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A swap is not applied until its recovery record is durable.""" + info = mcp_swap.CLIS["cursor"] + _write_json(info.config_path, {"mcpServers": {"libtmux": _pinned_json_entry()}}) + original = info.config_path.read_bytes() + real_atomic_write = mcp_swap.atomic_write + write_error = PermissionError("state is read-only") + + def fail_state_write(path: pathlib.Path, data: bytes) -> None: + if path == mcp_swap.STATE_FILE: + raise write_error + real_atomic_write(path, data) + + monkeypatch.setattr(mcp_swap, "atomic_write", fail_state_write) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + + assert mcp_swap.cmd_use_local(args) == 1 + assert info.config_path.read_bytes() == original + assert not mcp_swap.STATE_FILE.exists() + + +def test_swap_write_failure_keeps_recovery_state_without_raising( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A failed config write remains recoverable even when rollback also fails.""" + info = mcp_swap.CLIS["cursor"] + _write_json(info.config_path, {"mcpServers": {"libtmux": _pinned_json_entry()}}) + original = info.config_path.read_bytes() + target = info.config_path.resolve() + real_atomic_write = mcp_swap.atomic_write + write_error = PermissionError("config is read-only") + + def fail_config_write(path: pathlib.Path, data: bytes) -> None: + if path == target: + raise write_error + real_atomic_write(path, data) + + monkeypatch.setattr(mcp_swap, "atomic_write", fail_config_write) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + + assert mcp_swap.cmd_use_local(args) == 1 + state = mcp_swap.load_state() + assert info.config_path.read_bytes() == original + assert pathlib.Path(state["cursor", "user"].backup_path).exists() + + +def test_revert_write_failure_returns_failure_and_keeps_recovery_files( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An unwritable destination does not crash or discard recovery material.""" + info = mcp_swap.CLIS["cursor"] + _write_json(info.config_path, {"mcpServers": {"libtmux": _pinned_json_entry()}}) + parser = mcp_swap.build_parser() + assert ( + mcp_swap.cmd_use_local( + parser.parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + ) + == 0 + ) + state = mcp_swap.load_state() + backup = pathlib.Path(state["cursor", "user"].backup_path) + target = info.config_path.resolve() + real_atomic_write = mcp_swap.atomic_write + write_error = PermissionError("config is read-only") + + def fail_config_write(path: pathlib.Path, data: bytes) -> None: + if path == target: + raise write_error + real_atomic_write(path, data) + + monkeypatch.setattr(mcp_swap, "atomic_write", fail_config_write) + + assert mcp_swap.cmd_revert(parser.parse_args(["revert", "--cli", "cursor"])) == 1 + assert backup.exists() + assert ("cursor", "user") in mcp_swap.load_state() + + +def test_revert_state_failure_keeps_recovery_files( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A restored config keeps its backup until state cleanup is durable.""" + info = mcp_swap.CLIS["cursor"] + _write_json(info.config_path, {"mcpServers": {"libtmux": _pinned_json_entry()}}) + original = info.config_path.read_bytes() + parser = mcp_swap.build_parser() + assert ( + mcp_swap.cmd_use_local( + parser.parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + ) + == 0 + ) + state_bytes = mcp_swap.STATE_FILE.read_bytes() + backup = pathlib.Path(mcp_swap.load_state()["cursor", "user"].backup_path) + state_error = PermissionError("state is read-only") + + def fail_state_update(_entries: dict[t.Any, t.Any]) -> None: + raise state_error + + monkeypatch.setattr(mcp_swap, "_save_or_clear_state", fail_state_update) + + assert mcp_swap.cmd_revert(parser.parse_args(["revert", "--cli", "cursor"])) == 1 + assert info.config_path.read_bytes() == original + assert mcp_swap.STATE_FILE.read_bytes() == state_bytes + assert backup.exists() + + @pytest.mark.parametrize("raw", ["0", "-5", "notanumber", "1.5", ""]) def test_pr_number_rejects_what_is_not_a_pull_request(raw: str) -> None: """``--pr`` takes a positive number; anything else stops at the parser. @@ -2632,6 +2864,17 @@ def recording_mkstemp(*args: t.Any, **kwargs: t.Any) -> tuple[int, str]: assert staged_in == [str(target.parent)] +def test_atomic_write_preserves_the_target_mode(tmp_path: pathlib.Path) -> None: + """Replacing a config does not silently narrow its permission bits.""" + target = tmp_path / "mcp.json" + target.write_bytes(b"original\n") + target.chmod(0o640) + + mcp_swap.atomic_write(target, b"swapped\n") + + assert target.stat().st_mode & 0o777 == 0o640 + + def test_symlinked_config_swap_and_revert_round_trip( fake_home: pathlib.Path, fake_repo: pathlib.Path ) -> None: @@ -2662,3 +2905,42 @@ def test_symlinked_config_swap_and_revert_round_trip( assert info.config_path.is_symlink() assert target.read_bytes() == original assert not backup.exists() + + +@pytest.mark.parametrize("replacement_kind", ["symlink", "file"]) +def test_revert_uses_the_original_target_when_a_config_link_is_replaced( + fake_home: pathlib.Path, + fake_repo: pathlib.Path, + replacement_kind: str, +) -> None: + """Repointing or replacing a link cannot redirect recovery into a new file.""" + info = mcp_swap.CLIS["cursor"] + original_target = fake_home / "dotfiles" / "original.json" + new_target = fake_home / "dotfiles" / "replacement.json" + _write_json(original_target, {"mcpServers": {"libtmux": _pinned_json_entry()}}) + _write_json(new_target, {"sentinel": "leave me alone"}) + original = original_target.read_bytes() + replacement = new_target.read_bytes() + info.config_path.parent.mkdir(parents=True) + info.config_path.symlink_to(original_target) + parser = mcp_swap.build_parser() + + assert ( + mcp_swap.cmd_use_local( + parser.parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "cursor"] + ) + ) + == 0 + ) + info.config_path.unlink() + if replacement_kind == "symlink": + info.config_path.symlink_to(new_target) + replacement_path = new_target + else: + info.config_path.write_bytes(replacement) + replacement_path = info.config_path + + assert mcp_swap.cmd_revert(parser.parse_args(["revert", "--cli", "cursor"])) == 0 + assert original_target.read_bytes() == original + assert replacement_path.read_bytes() == replacement From 4e52d9261f12c229d6bc5f5f67953249ae07a018 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 13:22:19 -0500 Subject: [PATCH 14/71] mcp(docs[CHANGES]): Safer PR swap testing why: The unreleased note should summarize the branch's complete user-visible result without exposing implementation detail. what: - Lead with checkout-free pull-request testing and preflight - Summarize configuration preservation and recovery guarantees --- CHANGES | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index f7f2f536..fde47623 100644 --- a/CHANGES +++ b/CHANGES @@ -16,12 +16,13 @@ or as a bare name carrying only its type. ### Development -**`mcp_swap` can target a pull request** +**Safer pull-request testing with `mcp_swap.py`** -`use-local --pr N` points every agent CLI at a pull request's head, so -reviewing a branch across your agents needs no checkout and leaves nothing to -clean up afterwards. Swapping a config no longer rewrites text it was not -asked to change. (#115) +`use-local --pr N` points installed agent CLIs at a pull request without a +checkout and verifies the server before changing their configuration. Swaps +preserve unrelated config text, file permissions, and symlink targets, while +retaining the original recovery data across failed or concurrent updates. +(#115) #### CI actions updated to current majors From af1cd01df2f025a6d608bd328a74af2a20da249a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 14:20:47 -0500 Subject: [PATCH 15/71] py(deps[dev]) Bump dev packages --- uv.lock | 506 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 293 insertions(+), 213 deletions(-) diff --git a/uv.lock b/uv.lock index bdbf196a..04bdc6ea 100644 --- a/uv.lock +++ b/uv.lock @@ -114,43 +114,66 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.6.0" +version = "0.7.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" }, +sdist = { url = "https://files.pythonhosted.org/packages/c0/6a/a68c7f823e67714393060a0f232b0d75e0b2d2f1a7aef0633f7007411804/ast_serialize-0.7.0.tar.gz", hash = "sha256:934c0920454381b0beb46a5dc0af114d48699a44537b97282c2346a23990713d", size = 845507, upload-time = "2026-08-06T14:07:38.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/15/3b0e4edec45fd31a55243e37263236946cdfe6901e3f8cff934a443eddc0/ast_serialize-0.7.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:55e524f1329e2ee3f17d497b45d16a069afd2020b324ef6f32d21831e0b47a81", size = 1177653, upload-time = "2026-08-06T14:06:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3d/ecb5f748a3ff50153b5956200a8eea265797de98ebe7b6ca9f8f28d65f08/ast_serialize-0.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8dcbb7b94e5cb8c718f4f5c310b608fa73cd3714b819fe1995b20acfa6847689", size = 1167069, upload-time = "2026-08-06T14:06:09.49Z" }, + { url = "https://files.pythonhosted.org/packages/91/c3/f1fea5a1f115d04200b3b96c6865fae242b82d333589439262e7a561d4c3/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d1502bd29397daabf22e8d1afd97c794a4cbee2b8c6ae80a1e9912fa5421c9e", size = 1225358, upload-time = "2026-08-06T14:06:11.038Z" }, + { url = "https://files.pythonhosted.org/packages/79/ed/465365db4f2668a128bc77b6a5178eadfe29f572484f7817c2136c90996b/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d4a470e3ccdf5713960208379b248d18a6905e038929d430300396cfd0e937df", size = 1226814, upload-time = "2026-08-06T14:06:12.603Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c2/937f754ea0b1627265946d11161484331f9526d64552c6493d0bab7bcda6/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e0cb17de9c94a60e1b1563e2a0ce02c4b4843ce560d54efed6921dad4decbaa", size = 1424308, upload-time = "2026-08-06T14:06:14.174Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8f/3c03defc0a4154cef71f20aa3ca013118cf6a262141326b3ef4fa7860864/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d78cc7241483fbc291bab543541cc898084607f93ace74e9a61e553915be2398", size = 1244901, upload-time = "2026-08-06T14:06:15.693Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/0b51e6747bc82ae2a0269d6ed0f9c8bb79e82e68f5b4bea28ffd3e68c43a/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c61e55e70e4cadffafb2a0c0aa518395767145f926887de6464a8ecc7da7b52", size = 1248974, upload-time = "2026-08-06T14:06:17.135Z" }, + { url = "https://files.pythonhosted.org/packages/a3/03/8a31c5f41bde615e3d4e047a07fbb3f1f82461d0edaec1febfc2cb35bee9/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:13718805bce4e7423ce784c03a5595bb3e21b281d7206ee537d5660aea81d3b7", size = 1243730, upload-time = "2026-08-06T14:06:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/01/17/962c29a2b6a59a27dd7350e16488cd72ad70c86114535981ac8c8dbfd579/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:85adfb9103e1f7b6774a2342b93109e5748b7040edb888bd417b90f3037ebc31", size = 1293822, upload-time = "2026-08-06T14:06:20.143Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e9/886fe4d14de6c39e4c72e1e34d515ecc363dc51e93e3fc532f2e4fa14bbf/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8bad21d08510972269c8e3d1e331b4f304838fc5af715b8e4b355976e9f73717", size = 1401317, upload-time = "2026-08-06T14:06:21.795Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ab/8fc1de42b1bb4921da96a2fc23409d1a0e692fbdcb6ed9ed5a21a3b5a6a7/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:10993632ef5be1f96bdc890a2d1c1ef00aa4d1cab113d0302cde77862de35371", size = 1502261, upload-time = "2026-08-06T14:06:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/c158e0d25b2669492fa138dffbf799a6f8c189edfc057ca104c1a8549422/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c70991a4432d3a5e9efd30f9a3c824649d3631b334c4e1494a2a772cb62110fb", size = 1495376, upload-time = "2026-08-06T14:06:25.065Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fa/874086375aab22aed47ca07dbf351fb8d43ece6a87fc01e048267aba442c/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d8a702bb32455f9b8b9b845db33367790c8584d10677a422f30eb60eaf1ea7b", size = 1556461, upload-time = "2026-08-06T14:06:26.643Z" }, + { url = "https://files.pythonhosted.org/packages/59/48/6d79d19752a74cc263ee09792eb705b2dfdcab550c4b5118cfc280a68dad/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0d36fcd621bd4250fb10601dc70a29607442fc9bbfb345a0856df7c3c6efac9b", size = 1417646, upload-time = "2026-08-06T14:06:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/e8/aa/7d9577e52d62506017522d8699b4ee7105b307b4d56c5530d7b4d2982e93/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65f0f8299e5c02eaba2151ae0ccb7ba5d0061ad46563d1ddd480f5c0dd4a349e", size = 1445031, upload-time = "2026-08-06T14:06:30.173Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a9/b2342d382af078602488d90ff5d26013c8c9fb17826a6550f35cf34ee513/ast_serialize-0.7.0-cp314-cp314t-win32.whl", hash = "sha256:aafda49147e44f9d7a0ca12442fc8baef6926d09285aa920cf06355eed29c697", size = 1063728, upload-time = "2026-08-06T14:06:31.972Z" }, + { url = "https://files.pythonhosted.org/packages/98/b4/44b47d65ef69133a2615ab3558b44eb026b5eff08d8d5c548d771dfc7af1/ast_serialize-0.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ce58ca81e1cfa8faf58eca7dd35f6fe305928657288a9e7d9cb34970796d733e", size = 1103744, upload-time = "2026-08-06T14:06:34.017Z" }, + { url = "https://files.pythonhosted.org/packages/84/23/6c05b9f503bc5c4b6164868507bc3fd596e88e5634f34710b83ca108e91c/ast_serialize-0.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e5f6a61515511b82b211a7978ab76c1351cc28d6089aa946b8f9a9b3fa1d4ff0", size = 1076024, upload-time = "2026-08-06T14:06:35.642Z" }, + { url = "https://files.pythonhosted.org/packages/27/d6/4a95e85a3c52f10dba58e15f9c93ef691cf11a076cedf79817afade8d1fb/ast_serialize-0.7.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:c8c5af5650f2527e758f1fbaaad3deb37c91f1a2d01c7430c63100f51fbb2807", size = 1177734, upload-time = "2026-08-06T14:06:37.17Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9a/c2dca32e435b9c1006f030a65bf487f3bd2e74d38c0e29e1c4deb17ff4cc/ast_serialize-0.7.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:d8ba35d33b1fbd962a7afd835928b1741e90654c81344749b691b92e3aba98e5", size = 1169359, upload-time = "2026-08-06T14:06:39.206Z" }, + { url = "https://files.pythonhosted.org/packages/55/1b/30c73b248905b3de20d1ad11263a5eb5c7d8eec195e5b16e1b30f299225f/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f858a3f274f20ae65f1dccc9408de10c46a27ebf76eac5708793815f2f36182a", size = 1225641, upload-time = "2026-08-06T14:06:40.915Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ed/9f8d0c17598769fd07a3f57eda6a9f67eff729044067bff0e24ec32643f1/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db05954e883cc91310493cde193c1c1acbd00ee8ea583f1eff9be9740af50ceb", size = 1227063, upload-time = "2026-08-06T14:06:42.371Z" }, + { url = "https://files.pythonhosted.org/packages/73/28/42309bc14ca149f57320f6808c175689c656f1779ddaa64313dbf079fa38/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:507f5633ffec9d7cedfd8b90b986c329ceafb9deeaf4cd2bba9782affd83111e", size = 1425301, upload-time = "2026-08-06T14:06:43.891Z" }, + { url = "https://files.pythonhosted.org/packages/4f/75/493961f5deb0b02e688a83b8b9761aed76c8d655519f0cae8388d3ce4082/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1356c62fd91e73786e5f9b9de71f094c8d850d194f652de8c45ee12690534ee", size = 1245906, upload-time = "2026-08-06T14:06:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/56/9b/98350c0b7530218cf0db629ebc39c23f3c8b45f82d29be415cc5a455bab1/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eadaedebf5e3bf0d6dfa309c53283737c668a9e04ea39648e3607ee74cfef904", size = 1250047, upload-time = "2026-08-06T14:06:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3f/c8f2864f3d088bf0e41af2bb52462348a1d6c24c4fd0b4988b17c6594d78/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:1af0a4509871fd55f15335a5e8f643114c5c5cb485b90fcfa0d8b33861cdcc63", size = 1243423, upload-time = "2026-08-06T14:06:48.571Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/414c98fed866c0b584ef2d294a65561d5eaf5c58e8f23d099ea00f42e529/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8530f722344889785da4006f663735e470bfb22fd30eea20e89840ba3cb42fa1", size = 1294528, upload-time = "2026-08-06T14:06:50.157Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4f/b6b207f6fcf03b75c01605a16966e0133c3eb40e07c8e3dc66354ee7d550/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:80a1f859f8f5707848fd92c36daf01d522dbc3f2c066a2d9287e3bec0d79b4ac", size = 1401849, upload-time = "2026-08-06T14:06:52.016Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/676e2b65a919f39b6a04dae6ae863334561e0fbf6d30a403475f94203f35/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:8d5ceed027a42507a65f65746c9b1ada27dd898487306b2ae56b67af54fe5f28", size = 1502708, upload-time = "2026-08-06T14:06:53.707Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/d80bef4b92cdff36a58fc21eea99738af7087f8edc26f87c8e9102446d7e/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:ee63de1439b46de948996d81170d7c0b467d8c7068be44968c176539d05f7c36", size = 1496545, upload-time = "2026-08-06T14:06:55.141Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1e/1c7854a500b67d709f9185b3a5c14af6093e8c7b20d1b2d5c8a1edc1ea1a/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:90ecb3b1cebca24299eb84069ad5c0fb0e85d3f062167525dcc89a894641d619", size = 1558827, upload-time = "2026-08-06T14:06:56.884Z" }, + { url = "https://files.pythonhosted.org/packages/c8/34/e806b3768ec4249e36b000021cb31b441438e818141b654bfc1a7efc39e7/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:47cb2d836aa8905f3b14d47cb298f630968544829b93c7eb8a08337f9860e049", size = 1417164, upload-time = "2026-08-06T14:06:58.272Z" }, + { url = "https://files.pythonhosted.org/packages/d4/84/9dc9d0fd28324ee89212ec074973bb4db5b2ff826afb0a045f2aee813a36/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:7c54e159fbb62af577b4d20ac2ffb1f56ecfbe8c864a5222a4853adfdb245e25", size = 1446211, upload-time = "2026-08-06T14:06:59.695Z" }, + { url = "https://files.pythonhosted.org/packages/83/1a/02fcdac28c67ae7186a906b8b72ff8c94b7108b4d903a1e0a98260c3a295/ast_serialize-0.7.0-cp315-abi3.abi3t-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1dc8030604a7b1abe0ba3f31572e61312d5bfbafa0ab8cc255f8c285002a9d88", size = 862416, upload-time = "2026-08-06T14:07:01.247Z" }, + { url = "https://files.pythonhosted.org/packages/ea/72/840b2c14b693f40a69ba43c650a88e0dadea875faa1430c8b97dc0613d1a/ast_serialize-0.7.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:cef4d7fa14f6f259acf0c6cc4e9cd7a4ad773bdd13ba28c7d4127cb78e48d2c3", size = 1063825, upload-time = "2026-08-06T14:07:02.694Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cd/6de6248744d30875b875dd8e43a76427b0c2bf42b735ee26149002b9ee04/ast_serialize-0.7.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:537f7a41a7bc1108e60cf8d6f5575beafe5b8a17d1a6ef0e758b4d4e4ad90d18", size = 1105533, upload-time = "2026-08-06T14:07:04.068Z" }, + { url = "https://files.pythonhosted.org/packages/00/30/d17d123c6d6558fad5b7aa026d7df45a639370156cec03d8cf33be9401e7/ast_serialize-0.7.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:913ddfdbbfaa6294dc841579211baa00d789a60d9ae1ad5e04a1e98de11926e4", size = 1076322, upload-time = "2026-08-06T14:07:05.605Z" }, + { url = "https://files.pythonhosted.org/packages/73/73/329d080bb3f1ef96d852fa017617827d6edb38cad31abd0c5f5b7cb5df16/ast_serialize-0.7.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:832e23968712b5b5e052e2095cfe050d32030f2af2d58c6f2c733e8148c82c47", size = 1184035, upload-time = "2026-08-06T14:07:07.273Z" }, + { url = "https://files.pythonhosted.org/packages/9c/48/2bb83025fa197d38f3380357b70adc9f14bc12d87df062dfcbcfeb9e76af/ast_serialize-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7196e1351389df3d1f8e2acffab03db167a14b5bd1f108d8530171b0866f6f56", size = 1177582, upload-time = "2026-08-06T14:07:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7e/f9b13d64699eddc59bae3d027971e7b913750231249319898a4552228190/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a2343625ba33710f7e36a5be36e866ac7dbdd4369f11b5dfc6cd97d4aa85ecf", size = 1234638, upload-time = "2026-08-06T14:07:10.867Z" }, + { url = "https://files.pythonhosted.org/packages/58/e9/7fdbf053f3e35cb2a48d62f57c6a166e475ac9e7421c5004e0b602a7492b/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4251412007121afee236b654b80aca3a8072a073abe2d984c582fb507bf1aa4c", size = 1235796, upload-time = "2026-08-06T14:07:12.548Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/a6b29764a60036fe35dec206b08c4c69a184aeb76d5b0ba00d5ab5acbf6e/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76ca687ac87e97f8621d0be6f470aaa6307e4027ff1c449d36d1fb4f9fee1192", size = 1433051, upload-time = "2026-08-06T14:07:14.13Z" }, + { url = "https://files.pythonhosted.org/packages/73/c9/0eee1122c539407c2a7fbdd2461f33d3863fa20d38614f69f0cd7a6eca28/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:03e4fb60db9185c730db3521d73c6824dfd88d26ab2b2600a9ff1a466a79f0a6", size = 1255585, upload-time = "2026-08-06T14:07:15.682Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d2/33bcfd2507f247b15efd827818ff462b376dd3c637e1576e6f0211a6c0d3/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f058bd0b1e44276375730cfb11cd6beba00698dee2bb3d20b831ae319ca3d2b", size = 1258578, upload-time = "2026-08-06T14:07:17.135Z" }, + { url = "https://files.pythonhosted.org/packages/65/1f/61bfa3e75b50f5dd49cac0b8beb1e24d795d0648821bb531e97dff5c1a01/ast_serialize-0.7.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:54c8f5d009b2829bd98d7ebe0c29fcfc8f47f38e85d583d460837c54191b486c", size = 1253582, upload-time = "2026-08-06T14:07:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b9/2db828b7e1830411703e72626b692db540c26923c475f319d637657bb993/ast_serialize-0.7.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7f57b0a8ed664e163294e57b41c52c38527e5a0c73c01ead51f7073e1e652dd2", size = 1301023, upload-time = "2026-08-06T14:07:20.357Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/f93e40143f59ee37aebe9e5f8667f3c878232322febc03bb3b977afbf3d3/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a0f4ce65748e2ef28046324254e9b1ad088fae56a93d69d07fbddf97d875b85", size = 1410178, upload-time = "2026-08-06T14:07:21.932Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/a316dddccecedafea3002a9ed1cd6666ab5e64c3094951625b5a1ef95a1c/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:95219c374d0ac76639b26b66cca26e8cb090bf8c515d6bc376af484514d10151", size = 1509449, upload-time = "2026-08-06T14:07:23.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/34/c58f49cc9da933af6ee0f802eb780c48648ae9d0ee9188cb802c10dce29f/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1e71d8879b8c3480eb461128bbf0d5ff87f5ce368fcdb10c850a95010aa5ece4", size = 1505368, upload-time = "2026-08-06T14:07:25.101Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ce/4c5a9ff4e2851ec38ecb8aeef8cb7e0a02308616279c04429342e12985f4/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:85fa81ed590407c6ef6f85cac8e451051bf2623ecd875c4e124af8b8251de3ac", size = 1563486, upload-time = "2026-08-06T14:07:26.937Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/f239d17f902a2ce39da8e6dc034f48f45da93dfda658fa88189a02763510/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:fd018e40c9462235a1d2d101b420cff6c577e5a51b2954ffc05b907a0807defc", size = 1427995, upload-time = "2026-08-06T14:07:28.773Z" }, + { url = "https://files.pythonhosted.org/packages/62/6c/024f58d8b52ce29717c54a578f0bbd844439fcb8eb442596e756fd6eaffe/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:90a614b8c844620473b7445d31a2a6bf9500fdc4a8d1e2b6a70d40f38beea0e5", size = 1454215, upload-time = "2026-08-06T14:07:30.526Z" }, + { url = "https://files.pythonhosted.org/packages/12/48/f046778df64acef37a36a7338a9bdaa795f46b19d35a4d28bddcd8dfaec7/ast_serialize-0.7.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e6cfb423d4a14d774b3b82f6509adbc5da065246b6ec679221ad133820e0f7f4", size = 868142, upload-time = "2026-08-06T14:07:32.139Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fc/c862cc8d4d8d749f360035baa245fb3098b8b934c57122c0222ac5078762/ast_serialize-0.7.0-cp39-abi3-win32.whl", hash = "sha256:e818854e8521846f5a5271b1488c490231b8b6e02d0158ec42e57bc06dd764f0", size = 1068874, upload-time = "2026-08-06T14:07:33.895Z" }, + { url = "https://files.pythonhosted.org/packages/92/33/e846301850c18fa31598e342bb80b32f380c1bce285df571f5c3711960ff/ast_serialize-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:942921b81440d3ea57f90370d5d29bf28dc81c0778e26b736013e83c7b258023", size = 1111845, upload-time = "2026-08-06T14:07:35.308Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/4ee99be6306e72fac6051461794e5cd7895bbbdafe5b921a9a8c4b6fde1c/ast_serialize-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:43b73cf3924aa49f241be6e5f6a19943c8e2e07a2786fc719cacaa54ac6fd2d5", size = 1083656, upload-time = "2026-08-06T14:07:36.828Z" }, ] [[package]] @@ -545,100 +568,130 @@ wheels = [ [[package]] name = "coverage" -version = "7.15.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/45/78dbf9604ee5b3db24efbf26bed1cb58862fb40480cba821963c69348751/coverage-7.15.3.tar.gz", hash = "sha256:ae7ea5a4614acf399ef0483c4cb34f8f8f01df848d8fcbe7d3ce0865733f1c4d", size = 935592, upload-time = "2026-08-02T18:50:17.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/d9/01d8e19b2c0e55903bfb540c9f6bd32326f1d5b2fcb5a7dd8648ae2dd9c5/coverage-7.15.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3a82b2ceee91ba353e59fe2436d8a9eae799ff9825e5385423ea205d693e2949", size = 222202, upload-time = "2026-08-02T18:47:25.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/92/1c23aeb83c7239af07061abc6e96f00f9b62deec8fae022cab1b353e6d46/coverage-7.15.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3088cce65e54c2eefc08e7e1ca0b0acec1e95e8cf084ac848599103ed0367f74", size = 222723, upload-time = "2026-08-02T18:47:28.359Z" }, - { url = "https://files.pythonhosted.org/packages/b9/76/186f60bae815941553b70877d814c45994db8198bb76933bd062c18ee437/coverage-7.15.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a65e09efb0b5ab21fc54a8a65c5b2e533c0a4c0d064af0259a005dc656dc1b13", size = 249461, upload-time = "2026-08-02T18:47:29.802Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/53e010accfea3340905c5bb9207a2e461bba9b372621f1b88c1bd0e1392a/coverage-7.15.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b51f279a2477b0e1f288b98f141fd227acfdd1d3f0370400e473788879b47871", size = 251290, upload-time = "2026-08-02T18:47:31.445Z" }, - { url = "https://files.pythonhosted.org/packages/5b/13/d916056137fb6969e9d9f58ee11d1ef56778673843828315030733a3a0b6/coverage-7.15.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7835176988cbcf1f014db683bc33aa15e0558e412bf08deaa99757335b88df15", size = 253156, upload-time = "2026-08-02T18:47:33.033Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/0a4198d82e765f3351a91714d42526eb765e5c97776f7674489acbe7d062/coverage-7.15.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f24896dc8863167f6732f4142f5d37e6195eccc8fe5fe528d35d49597d29fdb3", size = 255068, upload-time = "2026-08-02T18:47:34.852Z" }, - { url = "https://files.pythonhosted.org/packages/00/d9/ebe4e0751e3637d87162887d0d3cdf4716f96782ab6face09a295e74cba4/coverage-7.15.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9490d43e5d041fdf376770a886a29722adb05f6b9c21a65c48c81fc8f1c33fd7", size = 250142, upload-time = "2026-08-02T18:47:36.588Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a2/12977c74fcf92f9b1da45fb9576c5f593a2c47bde04e937a8bb32dd56bfa/coverage-7.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:225e359bd5dedaff6d68e36091af20555866c557d968167308b677379bf575c3", size = 251195, upload-time = "2026-08-02T18:47:38.168Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c8/42bd9aa40386c0fbcc7af221ab4737dad10d27bb4971ca2783676619a79b/coverage-7.15.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:22119e2e3b2ac5ac024d50131fdd4b22ab4c6cf8aa2fc792cce73c0d94c5812d", size = 249200, upload-time = "2026-08-02T18:47:39.773Z" }, - { url = "https://files.pythonhosted.org/packages/0a/52/f1ce0dd8a2ec5c3911f1bc98b859be09cc4bbd705ed74ceb905729837c79/coverage-7.15.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:12d555badc462b0f6037ce8bec8b4af8d71f90eb55b57d0a358731f7ee7883e2", size = 253013, upload-time = "2026-08-02T18:47:41.372Z" }, - { url = "https://files.pythonhosted.org/packages/fd/2c/9a642c4cf7b6992b2eba75359b6cb548bd437001d6083fd0ffe492b80d38/coverage-7.15.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c4e2cf9cf774939b3dc581c6e31dfe7e8d7608b24f0f17524d6161f8235c3d2c", size = 249470, upload-time = "2026-08-02T18:47:42.997Z" }, - { url = "https://files.pythonhosted.org/packages/89/32/271d85639ac5de099046f7418e850047b1e964f893535128997b5cddde8d/coverage-7.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cea1b3e19d710f67e2ba9ce0b0b51032c2a9b4808a65ced48ddf336ef7e58058", size = 250073, upload-time = "2026-08-02T18:47:44.576Z" }, - { url = "https://files.pythonhosted.org/packages/15/71/6216430095c5437f83d7bfa7c1adb0965e26ada88d9fff49bf55e2cab154/coverage-7.15.3-cp310-cp310-win32.whl", hash = "sha256:25c77560309f157e7b7ee8fe0bf78d047ba900b7ae42f0e50e559305b366fea2", size = 224263, upload-time = "2026-08-02T18:47:46.078Z" }, - { url = "https://files.pythonhosted.org/packages/53/8a/f1032fb2714c28fedf00d73562c4bb9f713fa8a90593ed577bbb708a7de1/coverage-7.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:179fbf847e6c3d90ea71bfd570fe57f1ddb1c51474754894871c1e11099efaa0", size = 224886, upload-time = "2026-08-02T18:47:47.668Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9c/c8a3a923c24f631695cea2d5e2f02e776bc0af6e03800626e13a6c05a615/coverage-7.15.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5f3f854ab4599d98f7799ac9b91e34e8ec9ebc9a6372ee8c1f3413a68cc8b5e9", size = 222328, upload-time = "2026-08-02T18:47:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/92/51/dda77f34cbd2513d6ffb898c901d19e9ca55f48c0cbc4a1eb173a97d157a/coverage-7.15.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75268348fee1f199653b8a846262aec5581c6bb008c4f58824959fb708cc688f", size = 222832, upload-time = "2026-08-02T18:47:51.219Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/e0faafc4c6e23bd76c76148875ee9ec5781b8f1cd62cea2bc4ca0f0f0e5d/coverage-7.15.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21081739f6264cc594cad2d42b62befbd17633824022866c68720eb0c4b8d6b4", size = 253250, upload-time = "2026-08-02T18:47:52.737Z" }, - { url = "https://files.pythonhosted.org/packages/14/e2/4b1e0eeb727ffb471e411c1bd3402184b5dd54a77a762b0e55e87cdf9ae3/coverage-7.15.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:718d366251b060c10731c7dd359de6caea72250036eb94576aa56dacbf830a11", size = 255160, upload-time = "2026-08-02T18:47:54.404Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/a602d2d48f9db9f795e578a86aa914f7b20008e9330902defcfb73d17b3a/coverage-7.15.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa1bbaa502a6e877f3ee67cbac3eba2bb637f623e454e6c37b81b38896dbd48f", size = 257269, upload-time = "2026-08-02T18:47:56.157Z" }, - { url = "https://files.pythonhosted.org/packages/22/fa/bf6db13df2fcee00d2671849fe58c99232ee79a01fec7478c2bf7839b9e1/coverage-7.15.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:494880c9e60782610683f4eb9b65cce4f886673596b8f3cb2dfa079fc551c743", size = 259231, upload-time = "2026-08-02T18:47:57.76Z" }, - { url = "https://files.pythonhosted.org/packages/89/37/8118f13b17fa7d9a3aa2c301d93f2d5ffeef70fa7e27e639a74bdacd3fea/coverage-7.15.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3db264ea689f9e8f9fa4fb9005fee4048c3bff4a547f4cfa27f5086cb0804ec0", size = 253357, upload-time = "2026-08-02T18:47:59.261Z" }, - { url = "https://files.pythonhosted.org/packages/97/6d/c7b94fb03962f4d6f0fe13d01c4eb9c4c6e2e714a20d074516ec7582b110/coverage-7.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e869d4799674d67778e76ddbe2e26cf1673369262e231a8ec259421b1015fea", size = 254961, upload-time = "2026-08-02T18:48:00.901Z" }, - { url = "https://files.pythonhosted.org/packages/87/f9/fe0bd415fa56e36b62b649017c8fc98330858be4c7593789efb78cd24178/coverage-7.15.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:696fc7a28bbf717aba8d2c6963d26702945c7832cb313ba3b323aa5b1afb3156", size = 253024, upload-time = "2026-08-02T18:48:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/c1/7c/ffa53506d63ba8a77f5b9557dd6f5a5a5ad85adc680d7857410138f82bd9/coverage-7.15.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3fe9be1c527497d047f770d88a0110189714c36383bb88384508f750c302bffa", size = 256792, upload-time = "2026-08-02T18:48:04.377Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c6/df42458e72c18a49fe87e40ccd3fb0314210915256cf4a5593e1b3250e04/coverage-7.15.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2400591f4b2e33746c70846388f8bb4c7e33b820e31cb8c6cb2f25305310438b", size = 252744, upload-time = "2026-08-02T18:48:06.154Z" }, - { url = "https://files.pythonhosted.org/packages/f1/14/8bf18a4b10a44f8ba5f604b00e102f37daf49d581d66a37dc33fa267e1a6/coverage-7.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e557178799282269412a672e5753f2179edfe1b3f0f19b0c98f8e72d482326a", size = 253652, upload-time = "2026-08-02T18:48:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/27/e6/e530c9bb94e4155817cbd149034105b062a6913bc356ae08f454d155de53/coverage-7.15.3-cp311-cp311-win32.whl", hash = "sha256:68ea6c947375982ae907e19e9d2ef156bd6e68e11f3566dd568d7f4ec974e715", size = 224428, upload-time = "2026-08-02T18:48:09.845Z" }, - { url = "https://files.pythonhosted.org/packages/b4/98/0050c692d120988f1973a15196f52dee4ae221848b760281461a2005b613/coverage-7.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:28743dad31622e8c474b17446118037361f5b1f4f2ecdf72d4f6fde246d64446", size = 224906, upload-time = "2026-08-02T18:48:11.611Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ae/c0ef3e2ba3f35fc1c6985811a40edd9331e5b8978c9ecf84699de3edacbe/coverage-7.15.3-cp311-cp311-win_arm64.whl", hash = "sha256:c4398918c4fda32718191239e451fd86ac5ad1e8979b592f1921ee2d1f038965", size = 224448, upload-time = "2026-08-02T18:48:13.304Z" }, - { url = "https://files.pythonhosted.org/packages/d1/6c/bac99d9d4c6abe856e93bf3f5212982ac0bfac126dd4a042753bd53bc5af/coverage-7.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:79a3e32e83227d83d9684459ed579769b56c369ac2d7313099b2d9e031d2e10f", size = 222499, upload-time = "2026-08-02T18:48:15.018Z" }, - { url = "https://files.pythonhosted.org/packages/aa/bc/cb9a39b083bc1aa70586482dab25c9be20bab0ec6c155340e50d9066bb1e/coverage-7.15.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:767feb87c5886d781d0a69fafd450a20826ddab7b79bce1665deb64d21441b60", size = 222866, upload-time = "2026-08-02T18:48:16.884Z" }, - { url = "https://files.pythonhosted.org/packages/58/fb/beaa453d62000a0a5b39838bee2a137afe609a50a71f55e83c73461e513b/coverage-7.15.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50951e37033c40548d777b8a8454a2cd622dba1136780065678dccaec307c47f", size = 254367, upload-time = "2026-08-02T18:48:18.507Z" }, - { url = "https://files.pythonhosted.org/packages/66/64/43e72500ed6815cef189f9193f29d7af4b078830337c95ea976cd0c0d427/coverage-7.15.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:63a4ff67364afb2cac826b8bbd78a5c50ce656a7b7137436b44d7b96a9271088", size = 257103, upload-time = "2026-08-02T18:48:20.172Z" }, - { url = "https://files.pythonhosted.org/packages/66/3a/2893e2937adfe02f45fd38e4a8a0a0d8b7a02ff9e012ac3d009bee3c4f16/coverage-7.15.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95e42856509675fe26560310313a6117640e96f9a1e19bb3d220116a27c94c", size = 258220, upload-time = "2026-08-02T18:48:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/d5e6e2eb1a62961083734291304b1f85df72e2abe95c76eb88a7f472afd0/coverage-7.15.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:abad631cba27094b4631993f4c72e89ac0ca1b3a0236c7abaf8ca79aea619851", size = 260481, upload-time = "2026-08-02T18:48:23.682Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c9/9b72c5c6a9798a9a12cf65f66e077cc1fdd396e61915c862688f9afe1cae/coverage-7.15.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b0807f1f051dd82a234ad6acdb6f1425baede60be1e84e862496c8cc9262ab9", size = 254749, upload-time = "2026-08-02T18:48:25.32Z" }, - { url = "https://files.pythonhosted.org/packages/92/20/e1c2f759e2dbce559ba85c40c0e4acfecc6cff4b740c294c88e41ccc6111/coverage-7.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8d6df7aeb5bc464040bbc9ae173d875785d3677ebc4307817997d622d74225e", size = 256138, upload-time = "2026-08-02T18:48:27.064Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ab/48cc7e760f769e86ae290a125ea6e7209dfbdbbbb7ff4f5d9d1ee7a45d57/coverage-7.15.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:974471c506c9f5758808b47c1ebf7949ecd0848f5c1020e78675fefe5ff46866", size = 254283, upload-time = "2026-08-02T18:48:29.082Z" }, - { url = "https://files.pythonhosted.org/packages/15/26/39529a68154f99b3a1829debd8b25eac384effeec890a293b5bbdcb49186/coverage-7.15.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cba0c9c13e35c86df7998f1afaf6b1da224a3a39e4da59bdabf60c148046dcb", size = 258352, upload-time = "2026-08-02T18:48:30.892Z" }, - { url = "https://files.pythonhosted.org/packages/91/2f/55b82aa3d8d7dd8023a56e7c5c2a70e39a3c44b3353c6cf3faec9ad51566/coverage-7.15.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4d608dc36a364dce33acbf4fc3a50f9d2054c945f233bb0a2cdb4b90bfa17646", size = 253852, upload-time = "2026-08-02T18:48:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6d/839f4045124cd3518ecf2c58967e58a911202834e7c5a03cfdf2ab0b29f6/coverage-7.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2395869280554a1941da904423c12660c39f721315e1c02d076a7fe0971382f0", size = 255725, upload-time = "2026-08-02T18:48:34.848Z" }, - { url = "https://files.pythonhosted.org/packages/75/21/d25e3e2a9e327798078c877f469dfb6def860bf6e25036529046227d3e15/coverage-7.15.3-cp312-cp312-win32.whl", hash = "sha256:24f3b21840c3eb76cef3cc70b2bf6649010c64471a84a446538a39306e1ba04d", size = 224566, upload-time = "2026-08-02T18:48:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/b1/0f/df90cc1e8d095ce263968a93e04829821b2afb31ac2752c06a2e0a8e3c13/coverage-7.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:fa7b17902c3c1dd8a7adb52679b7f6340bba08443d710c8838e04db8cf62be2a", size = 225098, upload-time = "2026-08-02T18:48:38.941Z" }, - { url = "https://files.pythonhosted.org/packages/65/c7/ec49e43c58967a07163e2d1c6bbd58112b825b2772ab66784afd6a5400ba/coverage-7.15.3-cp312-cp312-win_arm64.whl", hash = "sha256:fcbe83fb7258eacd293bf5322d88807acb35ed12a5cfa99dd8215c083e3b0235", size = 224485, upload-time = "2026-08-02T18:48:40.682Z" }, - { url = "https://files.pythonhosted.org/packages/68/6e/62ae61e1fc434956bec38ed1d5b1c494f58cf579dbd998e77abffe7b3e6b/coverage-7.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1182eed05674c63d40951fae27c43e822749f04d25f75df64c2e4fa3168678de", size = 222522, upload-time = "2026-08-02T18:48:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/13/ff/c74c673d81e0e77b6608c3d21331e3db42e30daeb3c8a0a8860d4c9e2e14/coverage-7.15.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c0c4b0d7c4cd56e470d0c9d8441f42e8a96cdfd95050fec027f1d4dd9f11006c", size = 222894, upload-time = "2026-08-02T18:48:44.274Z" }, - { url = "https://files.pythonhosted.org/packages/a1/91/ccb30f5ffafd7d69d0b18e5162f9b711a5654e807b7b0c13497f0826b33f/coverage-7.15.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5c9fce9f4998b0d50a753da765b9215a14decc7863822c89d72da7a89ca625b3", size = 253890, upload-time = "2026-08-02T18:48:46.097Z" }, - { url = "https://files.pythonhosted.org/packages/29/c6/e92a66cda49a2751b09826d51258f199b92aa0cb005bc5f34e9729a52a9c/coverage-7.15.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a47e2a0a0ace9241e70ee00e44520f88b843094603dd54303f1bafecd929c30", size = 256484, upload-time = "2026-08-02T18:48:47.846Z" }, - { url = "https://files.pythonhosted.org/packages/96/7a/730929164b457cf25cf76c23898b90f9039a104a647890801b6586797b14/coverage-7.15.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95bad94f83807ae60ed76f3ac012f69b2605ac9ea81bee959a5a483f7fa09c10", size = 257723, upload-time = "2026-08-02T18:48:49.664Z" }, - { url = "https://files.pythonhosted.org/packages/9e/be/04cb5672cb19f5c389eda81ba22d89807699a949653d3625b0e0fda169da/coverage-7.15.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:228e172a76c428bb17d1ab78a2ff188990b0597e5dbd291f52a4edf7412de049", size = 259854, upload-time = "2026-08-02T18:48:51.413Z" }, - { url = "https://files.pythonhosted.org/packages/96/25/5e7fd6af39f6507071455944b8906dd1fe5b7b6bffb6a163ceb20afa0d13/coverage-7.15.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cea9fb33887c99349996266f1fd60abe5af3577a90633392001d27ef46b4b66e", size = 254085, upload-time = "2026-08-02T18:48:53.158Z" }, - { url = "https://files.pythonhosted.org/packages/23/c8/55e58a853f1e61163a6e755897bd14a059d78411e86560f39d9951c019b5/coverage-7.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81760de3155d7f52c21860c4046628dc6bed182f72e3c028e2b4fd46f65aa040", size = 255850, upload-time = "2026-08-02T18:48:55.031Z" }, - { url = "https://files.pythonhosted.org/packages/be/74/8bcec66dbcf3d22bea2a0b2b77ee2fa6f766a647d0023d4eabbc4f2b2756/coverage-7.15.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b47ea0a1d3a3d089826c6cbfad8429d7d8872e28e86baa95ddef330f6875da21", size = 253818, upload-time = "2026-08-02T18:48:57.163Z" }, - { url = "https://files.pythonhosted.org/packages/ce/06/450b673fdfece0997b4e16a31d6bde6b18889c578f1013ddd34c962ac6f9/coverage-7.15.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5459ba486b2a5d58a6c05254779ecdf525e7f20174d0210ceda75ba40fdb8f2c", size = 257973, upload-time = "2026-08-02T18:48:59.098Z" }, - { url = "https://files.pythonhosted.org/packages/56/fd/3ec7409aec0ddc943132452b65672f065f043b844f1830e1fe173c98b3ab/coverage-7.15.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c59209f80a08dbfcdd5109a80dc623cd3b9d22895c85757d34f57a6e6e95570f", size = 253638, upload-time = "2026-08-02T18:49:01.199Z" }, - { url = "https://files.pythonhosted.org/packages/75/20/30a8dabb194123631c93f860fdd86401ad405d56cfb1841873afbfe4e92b/coverage-7.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f863856c1779d4a5bb6a94698a2f9073e09c6706501f76f3e7780e72df97d21c", size = 255407, upload-time = "2026-08-02T18:49:03.143Z" }, - { url = "https://files.pythonhosted.org/packages/13/4d/e14365b1953b43653341412f9088b0d752614c626a73a705ff9af400f3a3/coverage-7.15.3-cp313-cp313-win32.whl", hash = "sha256:00cbdc5e322927dc30c5e42b863819b1bb867cc66f26ab5372c585850876ab93", size = 224575, upload-time = "2026-08-02T18:49:05.011Z" }, - { url = "https://files.pythonhosted.org/packages/1c/64/88f762ea80de2070207246faef514513be874486b2773528f2cc2b4b515c/coverage-7.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:835528518a1d823cf336740324b2f335f7c01e609e74abcb5d5163b3e66661e3", size = 225116, upload-time = "2026-08-02T18:49:06.894Z" }, - { url = "https://files.pythonhosted.org/packages/ab/66/03c34c53a319f522554cd29d4f2e16c5eab61aa4cdcf55753129fd7d926c/coverage-7.15.3-cp313-cp313-win_arm64.whl", hash = "sha256:0d2e1f2cbbf36b842f3e2aff8d118c60d677adb498bc6c7fa9c6838738f82767", size = 224509, upload-time = "2026-08-02T18:49:09.129Z" }, - { url = "https://files.pythonhosted.org/packages/35/6f/8c2dc014357618b3226c90f731b8282766c3685786f422558991dc49fbf2/coverage-7.15.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1e3bb08ad574bd9fb6a991f645728f70d333c1c1958dd5fcde65e24cb862813d", size = 222571, upload-time = "2026-08-02T18:49:11.242Z" }, - { url = "https://files.pythonhosted.org/packages/07/50/d867c7ceae9d56b7e74ee61ea834f1aa4f9a1e1c7f0ce39393ba573b1c12/coverage-7.15.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e5860eaff02a0b7f1b73304bdf846596ee62ab3a78d25c68044ebf684cb1fef", size = 222902, upload-time = "2026-08-02T18:49:13.448Z" }, - { url = "https://files.pythonhosted.org/packages/62/77/4f6dfc490c5f2bcacb2d296d9aa4d1e128c43b48e94ad313fec7f49f09ad/coverage-7.15.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:60874e5bd67f0b1bdbe42ab42c7bafa66a6fb8de88721af6df3f7a02713960cd", size = 253947, upload-time = "2026-08-02T18:49:15.304Z" }, - { url = "https://files.pythonhosted.org/packages/16/8a/6777f192af264165103e2a3d3768dbadb9894a0a2359a16877141d9ae8f5/coverage-7.15.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9147be876e9d83765e0b82176674dc248a6b9283e25e01e7462611b97e9b731", size = 256452, upload-time = "2026-08-02T18:49:17.801Z" }, - { url = "https://files.pythonhosted.org/packages/7d/7b/3d7ac46a0234bc684f41ee42be95e29b2b6525695adb04083609d5ac2149/coverage-7.15.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61a01f8c3804760fcc5a3d31c4f3cab792d660d44e17bf7adeaf0ea51e07821e", size = 257798, upload-time = "2026-08-02T18:49:19.878Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1e/c6ee59c29afcb5fdb35f936381340d1a06429a07c48f20e809646647acbe/coverage-7.15.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95bf3e7f26f792e25eb185f85a5a659d48479265176dcfe22b6f334fd0081b5c", size = 260112, upload-time = "2026-08-02T18:49:21.858Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e1/e8ea39a46e89e3a143312ee5f80336e992e3ae8fe44bf9c76b83fefeed42/coverage-7.15.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44c41eff9e413fed8740eca75d5438ebeb9d3e45e7cd37c67329213e7a72c764", size = 253944, upload-time = "2026-08-02T18:49:23.926Z" }, - { url = "https://files.pythonhosted.org/packages/95/67/31ab5f6a37fd887d1386f81f0da9306851ad2264e9baaa9c7f606e0b3e17/coverage-7.15.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54146bafb61f3ba9895b43af0dd17eba01561d586d44ce84ea221b0cbbee5a9e", size = 255805, upload-time = "2026-08-02T18:49:25.973Z" }, - { url = "https://files.pythonhosted.org/packages/fb/6a/ee505a80c8fd89620fb337c0596daecff87f33171fbb4ee3015fc3d7331f/coverage-7.15.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:af000dd1bb859ff8066fda4c79512ff938c798116540307226b373099c7b151f", size = 253769, upload-time = "2026-08-02T18:49:27.883Z" }, - { url = "https://files.pythonhosted.org/packages/b0/41/6ab0f81c9e89660230d8f3f581d4732e5ddb75a885b0a5dfc73d315dc94f/coverage-7.15.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a1b82490577f3889950b5a04f18712aef0207243e0749d60fe28c3c73ebfd5fd", size = 258045, upload-time = "2026-08-02T18:49:30.201Z" }, - { url = "https://files.pythonhosted.org/packages/bc/62/c995e91cae28cf31d6defab3bfb553dda5ac83ac7381b0f2b121264c307a/coverage-7.15.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c4fc90a60154c3e4b8a2dc206d6dbe852f1c235c249e0dc0cef909d032c9591a", size = 253587, upload-time = "2026-08-02T18:49:32.349Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/f2049980f82d6890321f2065f9e66216eabbf4b2001815db958bc543f40a/coverage-7.15.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f25bb884814a892948b4c20394db3f2364dd452d9492736479e7a493e63b0eb6", size = 255243, upload-time = "2026-08-02T18:49:34.324Z" }, - { url = "https://files.pythonhosted.org/packages/1d/82/2c841b67a978c0eb9c3707630b68f93f9e7585d78bb906bc8823ec6b07a5/coverage-7.15.3-cp314-cp314-win32.whl", hash = "sha256:722dbf8e7828fbcfe0dc8586167dc0a5ce85ad6ea171dbb21ed3f8d6581d3cb8", size = 224759, upload-time = "2026-08-02T18:49:36.326Z" }, - { url = "https://files.pythonhosted.org/packages/b3/78/5c93ec43784fd3e404ca23cd0584ae24bc1732de4a3fc194b68c3be88db0/coverage-7.15.3-cp314-cp314-win_amd64.whl", hash = "sha256:64d0845f9c3ed47302bed265c15ab4dbb64aa4ec1490839b8e328f4e7fa914d2", size = 225246, upload-time = "2026-08-02T18:49:38.366Z" }, - { url = "https://files.pythonhosted.org/packages/9d/77/813a054371f3b018cc63c6bdb46a3c35d5e95d4e3ed4f1449d4196106db5/coverage-7.15.3-cp314-cp314-win_arm64.whl", hash = "sha256:69bc14684f8fbbee9f9dbaa4fe79719b0da9725fc37956785c06ec365acf6926", size = 224673, upload-time = "2026-08-02T18:49:40.552Z" }, - { url = "https://files.pythonhosted.org/packages/8f/63/8c9f36cc71178d26db930baa03a4494abcc516d8d41bf820d0d85ef1d80b/coverage-7.15.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f92df943c24b96cb215ca26b4f6a2283e63c5db80f1635aceea7fff11311917b", size = 223298, upload-time = "2026-08-02T18:49:42.634Z" }, - { url = "https://files.pythonhosted.org/packages/54/66/211f24d058ce9f56ebf1420d55b7574fdae924f6da3836f83c8bd4793e38/coverage-7.15.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:66591c46bdd2971d3ae2bc503a5f0459c2edcaf6b7e045b292000cc95bc6cb95", size = 223568, upload-time = "2026-08-02T18:49:44.706Z" }, - { url = "https://files.pythonhosted.org/packages/dd/bb/9c2ad5574a0d6420a96c6cade4f8a683931b9e79fe609f8924d7b6964616/coverage-7.15.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa64458b81b18bfc67cdf1f6dc02b23e3edc672f2f8e11771fad75865415a43", size = 264932, upload-time = "2026-08-02T18:49:47.153Z" }, - { url = "https://files.pythonhosted.org/packages/ba/91/938c39e77bdd5a0a440412f975609ce3702dabbda6ac715719d93ca45a7b/coverage-7.15.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:447f5421ccf5475956cf516d4ca1d575f487947b6f4e11f9d80c6aefe24b3dc8", size = 267052, upload-time = "2026-08-02T18:49:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a3/7b431a98af35d9cc6394e54cde9435b33b8591672fbece6a4931267d7a8e/coverage-7.15.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0c77ef8cd483a4987a5d12d1d9d5f7ee598dfdc6c0844417d847e5768dc779", size = 269473, upload-time = "2026-08-02T18:49:51.599Z" }, - { url = "https://files.pythonhosted.org/packages/32/58/dbc9951dce46be47a732823a1c571f62bcabdd54a68d8c281489a1a55cfb/coverage-7.15.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b273f4ff657446a06c2d85bf80e134fa869a92852ba5f87854a70e1fb44da77", size = 270591, upload-time = "2026-08-02T18:49:53.865Z" }, - { url = "https://files.pythonhosted.org/packages/71/bd/1d610772c7c0889bfe477a59c46ee66ea53e271f3f06951e9d55b317f7c6/coverage-7.15.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daea8c4fafa22488600405be2c2be525a9406fba3fc0a83acc726db3e14e2005", size = 264007, upload-time = "2026-08-02T18:49:55.875Z" }, - { url = "https://files.pythonhosted.org/packages/69/97/852eb3dcdba156b1a9078503f098499916bf889f964b61ad4a08223ac169/coverage-7.15.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93ff57c530f3fa7aa69f92fb9b8892b8aa82712aa970842f4abf28657f42fb57", size = 266926, upload-time = "2026-08-02T18:49:57.944Z" }, - { url = "https://files.pythonhosted.org/packages/52/f8/b72cd238757fba2b587fc7dee047efe6e10b0c18343509faaaf502dd4680/coverage-7.15.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4df21bef8b800eebda9018f53d49c9ace3aeb0090c850139b27923aafcb83e91", size = 264529, upload-time = "2026-08-02T18:50:00.035Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0a/6c52ec4b7fb007cb6433d1fcfda4080cb15d75ad37ef9c31025f3427293e/coverage-7.15.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:db567b02685f26034adcbd85055f80d12cdf02111b8ed00886093d98b2874ce2", size = 268263, upload-time = "2026-08-02T18:50:02.161Z" }, - { url = "https://files.pythonhosted.org/packages/c0/4e/f1f9aa3efd109a04353563a43fb5155340c1fdcdeaa6296ebed3b6f510ea/coverage-7.15.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5318dd51b8600b947e058cf5a4fe54d183d9d13c49b97b64ca7be05a34df9bef", size = 263377, upload-time = "2026-08-02T18:50:04.243Z" }, - { url = "https://files.pythonhosted.org/packages/dd/fb/6b268a0b2728ef1c379ad656b899274477a5f6bed1bf6765b4b387fb0601/coverage-7.15.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c995bfa383c54704839b6c4c2627a1c00895597ada0e5e8190c81d8bd620555c", size = 265688, upload-time = "2026-08-02T18:50:06.428Z" }, - { url = "https://files.pythonhosted.org/packages/29/54/1a3ea96e5d5e7cd41dc432597bfc60692910e635d05e1cc25a8ccc243581/coverage-7.15.3-cp314-cp314t-win32.whl", hash = "sha256:6433fafb8da0e1d02eb53411e0ecdadb6b88f0224fdc23317e703c0e88937d42", size = 225066, upload-time = "2026-08-02T18:50:08.533Z" }, - { url = "https://files.pythonhosted.org/packages/31/9d/a7b0d9afd18ed5274dd00651a78e7810a931c70d94b79996f150bec1a30f/coverage-7.15.3-cp314-cp314t-win_amd64.whl", hash = "sha256:fe578952b1b29fe8c777f43f241d49efac4b56724a3434f5d22ebe3c208df429", size = 225897, upload-time = "2026-08-02T18:50:10.572Z" }, - { url = "https://files.pythonhosted.org/packages/ca/11/34c5ae40b945e69aa72b87dc268135b7049905f3824af573b7073acbb946/coverage-7.15.3-cp314-cp314t-win_arm64.whl", hash = "sha256:d2e1acb7aee29dfa8f3e48c23f36670898baca1209d9bdd3985a50c7f982165e", size = 225212, upload-time = "2026-08-02T18:50:12.63Z" }, - { url = "https://files.pythonhosted.org/packages/37/e7/7069b3d6c018917f49ba2e1c5fb910e498c7fefa3a1b78cb1b79e61ff45d/coverage-7.15.3-py3-none-any.whl", hash = "sha256:da78fa6fc7dafe4212839173133ee85afcf42c5cd5f3e47fa7c1c210453b445e", size = 214297, upload-time = "2026-08-02T18:50:14.709Z" }, +version = "7.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/70/b052a519a584663a7bd052841a2debe11c8309ec49a7786340003f9c0a02/coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264", size = 222245, upload-time = "2026-08-06T13:46:55.253Z" }, + { url = "https://files.pythonhosted.org/packages/67/39/892fa511aba3d1c3c8f49509a0ff5c71eab9f9f88d08e1a38da395821660/coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d", size = 222762, upload-time = "2026-08-06T13:46:57.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/95/b2c724ce1e64bc23cb5b1d7eeffa9548dc3d811f7a6297b2d01607f4e062/coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150", size = 249498, upload-time = "2026-08-06T13:46:59.012Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4f/b1973f67a1382af65b572a31ed692f8e490a6ad707191eab59148376832a/coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15", size = 251328, upload-time = "2026-08-06T13:47:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/03efa6722a132abcac91b32a60b64b240dd707c189c64eee697e48992c96/coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0", size = 253194, upload-time = "2026-08-06T13:47:01.976Z" }, + { url = "https://files.pythonhosted.org/packages/45/63/8299201d9c80fb65551ce99c966cab83d706ec4066ac999bef08201346de/coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d", size = 255106, upload-time = "2026-08-06T13:47:03.281Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/26fd8a691eb8d9a230128685f6d23309d7402cb030aa553001788c8c50fc/coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4", size = 250177, upload-time = "2026-08-06T13:47:04.713Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ef/3c7556f33783a0a566e01443ca62bd8eb2cdfe22d271efdc02e08beb5654/coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54", size = 251234, upload-time = "2026-08-06T13:47:06.104Z" }, + { url = "https://files.pythonhosted.org/packages/29/49/640a34043edac950738f36a3567832db5731d4cb2ed84b59cdb89c6bccbf/coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20", size = 249237, upload-time = "2026-08-06T13:47:07.467Z" }, + { url = "https://files.pythonhosted.org/packages/48/f5/e80f212669dd1be954ff844f883ef11a437ef4fd0089c6e0effc7b66b15d/coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325", size = 253050, upload-time = "2026-08-06T13:47:08.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e9/e5da0fe39f7fde1bca9edc09c60921bb5fdba4cec7db5bbad41ddfd8c230/coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88", size = 249508, upload-time = "2026-08-06T13:47:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/41bf25774a0c8bba6b467f917cb1c9a0a2605e02dc93aad489fc7050ed59/coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04", size = 250110, upload-time = "2026-08-06T13:47:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/26f2e54b79acc29d179ee4272922625aedb69198c4eb61f7ff4f098f3c78/coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338", size = 224294, upload-time = "2026-08-06T13:47:12.753Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/9a318fc3ae040d4d6cb2d86101c6aa963fab20899a5c58666adf52cde0ca/coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360", size = 224919, upload-time = "2026-08-06T13:47:14.17Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/fb/65/ec03b743a2a229c72cc1eff3e57be9d3564e9c6b4d5aba2d70744a3fc0d8/coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6", size = 255199, upload-time = "2026-08-06T13:47:19.765Z" }, + { url = "https://files.pythonhosted.org/packages/41/4b/5163729e4b6582d61975cfd3ccab45b4ec53e21cf156d9941cb025188468/coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d", size = 257308, upload-time = "2026-08-06T13:47:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/2167a0f08fb87d702fa423a48578a32865464b7c9e1db3911ad7812ab414/coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce", size = 259268, upload-time = "2026-08-06T13:47:22.503Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e5/68eebae3053dbd48508edea559c21b23fbdf3460784f91370c83a86a6acd/coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7", size = 253392, upload-time = "2026-08-06T13:47:23.88Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/fd4ced40a2b691c774e515c9b69500bfa64c7960b67fcee4b2f6fad97fc3/coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b", size = 255001, upload-time = "2026-08-06T13:47:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/ae2e5fa710bb6957a9aadeb9e3598d3b3e4af6587ce857ad42e8639a3f30/coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc", size = 253061, upload-time = "2026-08-06T13:47:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/d7/31/67ddc0365db2c6e93ac8580bc4bbc50f65273262f973f63ebcdbc15c0495/coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571", size = 256831, upload-time = "2026-08-06T13:47:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/82b8fd18f57fb13f12d98fe874995bb2c4f9f17be8aff762c426323fdb96/coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719", size = 252781, upload-time = "2026-08-06T13:47:29.712Z" }, + { url = "https://files.pythonhosted.org/packages/0a/eb/6c74ef4dd12b252e573c49bdef9e2ac265bf3dbb79b8d7feb3266e084e9e/coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7", size = 253692, upload-time = "2026-08-06T13:47:31.192Z" }, + { url = "https://files.pythonhosted.org/packages/5a/66/eb9aed1c3fd2d36ee00eb173f434b14fa607fc056739c9a89ff4244010ea/coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e", size = 224461, upload-time = "2026-08-06T13:47:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6d/81fa4161dfb3ed9d74e40d58647eff83a56b7612e78352581280fce2f477/coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc", size = 224937, upload-time = "2026-08-06T13:47:34.205Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c1/d8dacf683c6cad3cf85ce68fd3774a6774ec402128822fdfaed920f11e6a/coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890", size = 224479, upload-time = "2026-08-06T13:47:36.118Z" }, + { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, + { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, + { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, + { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, + { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, + { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, + { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, + { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, + { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, + { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, + { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, + { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, + { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, ] [package.optional-dependencies] @@ -1140,89 +1193,116 @@ wheels = [ [[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/2f/ec5241c38e7fa0fe6c26bfc450e78b9489a6c3c08b394b85d2c10e506975/librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5", size = 148654, upload-time = "2026-07-08T12:24:30.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/1a/d651e18d3ee7aa2879322368c4f278bb7ecaa6b90caadfdec4ebfa8389f3/librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547", size = 153537, upload-time = "2026-07-08T12:24:31.773Z" }, - { url = "https://files.pythonhosted.org/packages/45/18/10bff2122577246009d9619b6569596daf69b7648812f997ca9ca0426f60/librt-0.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2", size = 494336, upload-time = "2026-07-08T12:24:33.079Z" }, - { url = "https://files.pythonhosted.org/packages/67/69/87dfee871b852970f137fdeae8e2ca356c5ab38e6f21d2a3299535fc3159/librt-0.13.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929", size = 485393, upload-time = "2026-07-08T12:24:34.324Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d5/625447a8c0441ff5f15f4ac5e1d323fb9d4d256ebfde7a3c8e003f646057/librt-0.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a", size = 515382, upload-time = "2026-07-08T12:24:35.575Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d8/1c8c49ea04235960426444deece9092a6b3a9587a850a81bae2335317411/librt-0.13.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac", size = 509483, upload-time = "2026-07-08T12:24:36.923Z" }, - { url = "https://files.pythonhosted.org/packages/6f/65/f1760fc48050e215201a03506c32b7270159088d01f64557b53e39e74a45/librt-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7", size = 532503, upload-time = "2026-07-08T12:24:38.203Z" }, - { url = "https://files.pythonhosted.org/packages/18/1b/793e281dcf494879eff99f642b63ebc9c7c58694a1c2d1e93362a22c7041/librt-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40", size = 537027, upload-time = "2026-07-08T12:24:39.34Z" }, - { url = "https://files.pythonhosted.org/packages/69/45/0801bbb40c9eea795d3dd3ce91c4c5f3fe7d42d23ec4be3e8cb283bcc754/librt-0.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a", size = 517100, upload-time = "2026-07-08T12:24:40.907Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6c/eb5f514f8e29d4924bc0ff4601dd7b4175557e182e7c0721e84cffa39b8a/librt-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde", size = 558653, upload-time = "2026-07-08T12:24:42.359Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bf/f140100d1b59fe87ff40b5ecbb4e27924335b189a784e230ee465452f6c2/librt-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8", size = 104402, upload-time = "2026-07-08T12:24:43.668Z" }, - { url = "https://files.pythonhosted.org/packages/22/7c/57e40fef7cfb61869341cb28bdcefe8a950bebcbecca74a397bae14dce4a/librt-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc", size = 125002, upload-time = "2026-07-08T12:24:44.793Z" }, - { 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" }, +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/ee/999b97f4b8eb28c4416f9f8a6708077daa1639145cde1c25ac76517df966/librt-0.14.0.tar.gz", hash = "sha256:474eedc5e910d88c1a12193a238f69b2522561d6f10bda9fbe9e70961d2e64e3", size = 214292, upload-time = "2026-08-06T14:52:21.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/fe/c84ae5dec0b6fd9a6f3ea5c9aca7a9a96f1e2b7ed9a713eee41b692e2ec7/librt-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dd8270544defd2a25e57f4bf2ad94ff62a74addf7e82a479eea4ce9c5687516", size = 148643, upload-time = "2026-08-06T14:49:19.233Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6d/5248bfe9260b699495f414f7bdf7e52fc7dc7c9a6124bea563182f9d3a32/librt-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36044fb53cc1aa274a69406b67c7c8d90511fdf01389a6cf6d04f7b9bc57f067", size = 153536, upload-time = "2026-08-06T14:49:20.804Z" }, + { url = "https://files.pythonhosted.org/packages/16/b5/be43f0e5cb1ac9601ed7c8416b20822d28784653115369233edc1f268f7e/librt-0.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a2571b37e8eaa23c2aa886698ab5bff51f3d8770a9e83dad44c948ee102aa4f", size = 494314, upload-time = "2026-08-06T14:49:22.259Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9d/69384d4b45273e84089b7fd1c5d96bb66d5ecb0ac41e0070b04e22034821/librt-0.14.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:3b63b5469755d61c634e00223b95496e3098efcf8c7cb68c8cefd4e5294cb289", size = 485394, upload-time = "2026-08-06T14:49:23.855Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b1/2dfc0cdff4e07c473e69997e8db397851470f7a7ab32f30beff4d25d0238/librt-0.14.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1b29b29f92f8ea8b5e8aa427a553786a368726c1f266a5f3116a0cfdb300acd", size = 515383, upload-time = "2026-08-06T14:49:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/cac6921dc1f5ec90e8eeb77a9b49b661d61e0a1c155b96847341fdae88ab/librt-0.14.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2767207d65ca0cf795d60fa67d3fd027ad6612a65c7c9a8286d9cd5a9bba0d41", size = 509450, upload-time = "2026-08-06T14:49:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9b/063e66ea495b894107a7c254ba878e8217ec3312745a795a561d9c9ae693/librt-0.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5ccbd6c9b6a25e5fc2c741691bc77070635ef337b437b8d41bbf1ba917c848a2", size = 532490, upload-time = "2026-08-06T14:49:28.167Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/327b48fd29598f6bc978b37051a013079d1bf222392975a73be542d62d5b/librt-0.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:42fc770d647dbc26f3424715fed2a298e5adcbdce2390f463f0494c0114c6fca", size = 537012, upload-time = "2026-08-06T14:49:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/65/f3/6da0649987c00de52e0389d84bfbf2f87fad367f4ff9ea77b47915fb12e3/librt-0.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7c994b599682ec83e711aaadc0e00b12adc38eaf2d7fee814ef81572332d6676", size = 517105, upload-time = "2026-08-06T14:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/89/77/27fa7c9752d0fac645226c2a6c9453f0f755ab5bb4127c6f6277b148c3ea/librt-0.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89179ae255fd9404ac2a423aa58458498518240dfe96dd5e779b4876a83c52bf", size = 558629, upload-time = "2026-08-06T14:49:32.912Z" }, + { url = "https://files.pythonhosted.org/packages/fd/64/9a22a05f8e5292c73d049e923005db8242c29d8c419af34118769775435a/librt-0.14.0-cp310-cp310-win32.whl", hash = "sha256:100cbca2c49533bf5b2cd449d6d499404ae74ecdcb14b337e5547a1404beb9a7", size = 104396, upload-time = "2026-08-06T14:49:34.275Z" }, + { url = "https://files.pythonhosted.org/packages/21/77/a2536afe1c13f16e272d556921ad073d7dfaf3f1a565e00df95bf634bb60/librt-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:e9d9c86af6ae6647abd2f7161e7b2d26d20d0bedf9208b48dddd5f67f13cfbc9", size = 125007, upload-time = "2026-08-06T14:49:35.626Z" }, + { url = "https://files.pythonhosted.org/packages/51/9f/69010fbc4bbf0a19860398340ef46bd62a2c2ac74105a3f409e96500bd30/librt-0.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b77992d446fcf9fcefee57103786f45bb88009b45156863f4b47cd21c6e10c9", size = 148045, upload-time = "2026-08-06T14:49:36.878Z" }, + { url = "https://files.pythonhosted.org/packages/c8/35/dec33c00efadb83cb666d6bcf8561694cefc6f471f0542c22c0840f4cc5b/librt-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ccf121eaf0b28f83221c8c754006293ea4a5e4afff0dc0ebdc90d48520c0972", size = 153028, upload-time = "2026-08-06T14:49:38.297Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/a91d4a6802fb31068738ef37833ed4a15f007a70e8b77f43a637312caa6e/librt-0.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4db93f3c82a0fca78360a52da98ee1709bfe369b5bbf935dd064f57753c7085", size = 493046, upload-time = "2026-08-06T14:49:39.778Z" }, + { url = "https://files.pythonhosted.org/packages/c3/21/b3c71e5bfc4089a71a76230e6e163a770d9df65c1221c66d274b9b14f111/librt-0.14.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:04f1ece0f80171c4cfef795fa7bf4a75ffe79ec69cc715027d690c8c9b6166fc", size = 485498, upload-time = "2026-08-06T14:49:41.368Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b8/7003a2c56d34d0ee104b292361d442db1f0fcd4679c53fc2a6112a532097/librt-0.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61aca1a405f6d97ec93f676146265677fba3f7c19f779f16d496162853b7816e", size = 515912, upload-time = "2026-08-06T14:49:42.834Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e0/8f0f4bebe4affe3c073bd7ca52abfba3af44565a435f3ef01406c4624495/librt-0.14.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69b592abaa76483401de68c6f52e0ca37687807cbc9928aa126bfd8c11e61b68", size = 508569, upload-time = "2026-08-06T14:49:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/fd28634391dc1899e0714d67ba935d135025902233c1a6854dc761fd9f15/librt-0.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:595bb8f3c5193cdc7235ad2ab7651b2e823de27a1c4cb83029a57da1e11ce326", size = 530361, upload-time = "2026-08-06T14:49:46.154Z" }, + { url = "https://files.pythonhosted.org/packages/7a/33/b8aecde080731781016180fed0b830cbb40ff9a60a30fa28983c6585315b/librt-0.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ffdb67c2a6925dc4f191771ce9b279c496953639730a343ade0631b222551b8f", size = 534232, upload-time = "2026-08-06T14:49:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/c5/7c/3bd4aa098a4dbbe8ef6b3c851a91d281e19b1785593de3ff5b06571e31a4/librt-0.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5c320c201223605656bda3f07c0536e7629f91a79d84167ee1b71257f0b86921", size = 514269, upload-time = "2026-08-06T14:49:49.013Z" }, + { url = "https://files.pythonhosted.org/packages/61/e2/596e551af59cbc6795dab12653e814b7c069f67ab0134214f285aa945cd2/librt-0.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0660dce4717a345319824ae99bc9036ad887ba5a189422a8c6431bf63d2947c4", size = 557582, upload-time = "2026-08-06T14:49:50.387Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ad/abc3f8fe20618babe70a9864ccaa17b76fa5b7570fc06b35a17315328718/librt-0.14.0-cp311-cp311-win32.whl", hash = "sha256:bd79b5c8a3f09abdc77dee7fea06427bb4cb5fe8e7151029ff9799c75886137c", size = 104900, upload-time = "2026-08-06T14:49:52.002Z" }, + { url = "https://files.pythonhosted.org/packages/fc/04/15bf402734bc8237ce8489cd3ab059810d6c11f19ad04f266bbe5c363e52/librt-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:5878e3f8a09e5dfe862a58f4665c3e56912ab74dec6b3cb74d336e8136ab2141", size = 125843, upload-time = "2026-08-06T14:49:53.47Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/5c8d94429511443140aa02188a746544e0f47983a7fc62e2767a7d6c7b18/librt-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:0d907a964a0ad582f87726cf4a71bf60f710bdfb2550adbe52070bb88bf1dc4f", size = 111831, upload-time = "2026-08-06T14:49:54.732Z" }, + { url = "https://files.pythonhosted.org/packages/78/7d/ca9feff7e486d74ae3efb5bc66ab95d15fe29090bd902c24be660deed7a3/librt-0.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:171dd324dd6b269503e3777b9cc66c1c6f7bbcbf9b9403b96c5901377bde8f4e", size = 150997, upload-time = "2026-08-06T14:49:56.049Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/9452106e9b2b0f5c771c5656af763927c351b60363e6496fff2775280507/librt-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c648d09e7842f42cb8bcae41b6a39d6536448612ee4c516ce58b284300f5a066", size = 155241, upload-time = "2026-08-06T14:49:57.344Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5b/eeb378de1b84761d555e100e5c69facfa6cb6266ef4a11baab55d64ac6b7/librt-0.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9bb36c48a582caf7d604bc3f3554b550431d1aa6b131a618e33e2816261620a", size = 503094, upload-time = "2026-08-06T14:49:58.775Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/e5c372c7c543cbe721b25ac6837190f27d627838a9208b7069e167c06f9c/librt-0.14.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:93c52e7d5f9ac110db854a4c49cc5fb0362e77047e7ebf71f1a6f35829395d47", size = 496536, upload-time = "2026-08-06T14:50:00.43Z" }, + { url = "https://files.pythonhosted.org/packages/e0/81/9d64cc59740a37a68e4bcad1fb1734a354c6e2899cc66871feff4c1a9032/librt-0.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31e9931b28896420c0870332eefc8e966121adf8013850d14a22d694c17c8cdf", size = 531811, upload-time = "2026-08-06T14:50:02.018Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/c643336b08dfd38e27a77ad3811ec7f18c2aa83677332f21300430ad5fcb/librt-0.14.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:52e3630f4d00aa1fd1c224e2ae7ea2a31058493247f765393b7aac2bd0f3bf01", size = 524425, upload-time = "2026-08-06T14:50:03.528Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/aa78ba06cf8546a0f3d5ef6985b721d4bad60f1d938e147b6576f826dae7/librt-0.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c66f6183f4dde136d5567fbd192c86236c8a89caa73f2720f2ed94c176194041", size = 543060, upload-time = "2026-08-06T14:50:05.15Z" }, + { url = "https://files.pythonhosted.org/packages/f5/18/a21935834a687940ad83ecfc14aa9118493f0da53b6e9861082f8cae2381/librt-0.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62d566b4b4f6510d471fb6fa1b6bf8d183a1039d2c058d85f7481ee24ee3d27d", size = 546840, upload-time = "2026-08-06T14:50:06.571Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d1/98915036feb2315cabd700f25d9915969f81a8391d2cd9c0cc93df6f7116/librt-0.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:069a5790f1ba9b6abd882033a486b5a5b68754854b7ad015ee5ad8da1d23e11f", size = 535732, upload-time = "2026-08-06T14:50:08.478Z" }, + { url = "https://files.pythonhosted.org/packages/40/e4/280b07ef374464ca550523ed82049bffe8e06f73d11ee947543389b3cd23/librt-0.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2ec4541a9e522871fa2f2983478d7ee9cd994af1e4718b7e291904c15430b062", size = 573579, upload-time = "2026-08-06T14:50:09.949Z" }, + { url = "https://files.pythonhosted.org/packages/9b/75/773489183b257cab2f341087d9327155f7763c1fab07984a8d748554bdfa/librt-0.14.0-cp312-cp312-win32.whl", hash = "sha256:1697ebe1612604233cd69383c4369c91a40f7d8ad1bd890e1647acec35700f32", size = 106097, upload-time = "2026-08-06T14:50:11.414Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7c/f60a3379723295761403ae6301be16afa98b389dee8c6aa1b5de35794d76/librt-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:3e30f225dc2598638a03bd0fd56d479e09263fd769298af57711c2c921498e59", size = 126933, upload-time = "2026-08-06T14:50:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d5/08147d10a6e5ca676dc0e140b10758a83de73fc863712f167751f2aa8bca/librt-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:d6c98dd32e0f8d1a701bcfd470de0d09972d725c8dbad3cf9791b5262caae1a4", size = 112237, upload-time = "2026-08-06T14:50:14.12Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1e/eb17e048ef320dc3c780f883c25e8bbcb32d9b99baee4df47743dd19fd83/librt-0.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e483dcacff2ebef0e390002475fafe026f1171632e88e1a8bc69bbc4fe31d92", size = 151028, upload-time = "2026-08-06T14:50:15.505Z" }, + { url = "https://files.pythonhosted.org/packages/48/0d/a08a4e73990d401031d17f2ac25de61e32efbfc2f195903a5f41782ea2ec/librt-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7a469d3a638cb1310f177ad6263478661c31929628e04ad8c64eae43117aa935", size = 155140, upload-time = "2026-08-06T14:50:16.866Z" }, + { url = "https://files.pythonhosted.org/packages/73/97/5fbe0c8f05a549678180c56f0792aaa85462c4184645a0dadb720e1e7edc/librt-0.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a97882a260cc133838c4eb5083c1d39c6dd9c5bb0ef88f052f7ee078cf132a", size = 502531, upload-time = "2026-08-06T14:50:18.292Z" }, + { url = "https://files.pythonhosted.org/packages/0c/19/b1124bbc3b53884726b36feb5893f17c64638560f363e474af1ca808a961/librt-0.14.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b8bdb74dc214de53fd337adc08d01decf478c94b98eb71de8175476499b5f094", size = 496114, upload-time = "2026-08-06T14:50:19.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/1e/ce212234460b1420d223c6d531579e6192d2529624cd4037fbb46da0041d/librt-0.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b75736fc71bf792451f3c09f6924e3f1a456d60bd9e522205229f6a4501b1dac", size = 531575, upload-time = "2026-08-06T14:50:21.313Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9e/13a82455687f65d52f886b4189d1f1546d55c76441984b490f4fea0beb44/librt-0.14.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc4f4191d97ca3e5d1c9dea91b9c363e727ca01a4ecd8cb75c150d262e0be6e6", size = 524444, upload-time = "2026-08-06T14:50:23.043Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/30b4b024010410bbd2145e8f1d09568465bf0e9a8ae74b689bb605cb1567/librt-0.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:079407feefd3746de68a73918e9ef43d0c5b791a3b198a9488568fe42baf57f8", size = 543090, upload-time = "2026-08-06T14:50:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/ad/08/befe0e170b1063ac49d4819c2d4854698656a9dd404c4c9d62e00b426bf2/librt-0.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67d756804347354df8bb9bf1e9f8b7565b4d0528cf14406d543937bca04d8545", size = 546405, upload-time = "2026-08-06T14:50:26.192Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5d/e55773f575c8ea1d33b29c2b6883f05ee109a112c764f7127a09e0c288c9/librt-0.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f91abb5c73148dc49188ae47341902432893ebca5cf12c60f03b25615ac906a2", size = 535995, upload-time = "2026-08-06T14:50:27.829Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c2/83730da76d7e273163da50184fe04cee3678fe65f7c8002fe33811c5a621/librt-0.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7574c4ee6d9df5a3de33d54b0e93c4a069a4671e9a757c7ca8fd93a8635d16b", size = 573590, upload-time = "2026-08-06T14:50:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bf/22e882115f94276a7d922c63af87ffbd0cdf5e3f545d3dd75dd8e7a9614d/librt-0.14.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:184b24430f480b4e4f910cd5dbcd22eb1ac2ef91aa98822a774acc032ed17ec3", size = 82189, upload-time = "2026-08-06T14:50:30.571Z" }, + { url = "https://files.pythonhosted.org/packages/62/70/0389b1e1a9ee1ed73751cb18decf3a33bdd19781c9fadb46415271720081/librt-0.14.0-cp313-cp313-win32.whl", hash = "sha256:48112eff5a4fecd8919260363f35009354909a48bc49c100ca2a93b6b344ea46", size = 106198, upload-time = "2026-08-06T14:50:31.794Z" }, + { url = "https://files.pythonhosted.org/packages/23/19/c38ecd86f649a1014bc0bfcd9c6ce620491f67a42975929dbeb87409e9a5/librt-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:6ed36b53455622ea42b20fa776f6ab7fd15225b417cda80d5def848f66a692c7", size = 126963, upload-time = "2026-08-06T14:50:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2c/9b485d945e64e94cdc703aab7a2b2e88dba27c8b1bb1667ffb301476812f/librt-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:9b24351679fce00a3609505d6898a098f554ea00dcbd2babe89fd8710bb27809", size = 112127, upload-time = "2026-08-06T14:50:34.331Z" }, + { url = "https://files.pythonhosted.org/packages/0e/99/47fcbdbdb5031a90533573293fad645aada913e4da402123586622c313b3/librt-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:514e125185753c938686bc2262a4a02598430e32409d9a986af518826c870d0a", size = 149815, upload-time = "2026-08-06T14:50:35.693Z" }, + { url = "https://files.pythonhosted.org/packages/63/c2/4ce5142e0056c5b2334cff0173b8ad4c79f67b75b612eb9a4139a560cdfd/librt-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b15f24c285a6718f284da5cbf9f199222618bc3830715f3e6f7188fd76b27dd9", size = 154047, upload-time = "2026-08-06T14:50:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/18/e4/c589881658f624873f7b11d4e834c3b84e103a3cef83b8fd8074907f0d30/librt-0.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:522ef1b2de9293d43edf5a5aea38a7ce32c747c6411e7b5629e23bc5346d7d3d", size = 494157, upload-time = "2026-08-06T14:50:39.615Z" }, + { url = "https://files.pythonhosted.org/packages/10/a5/e100af06bc6310e1a1d714ff923425f2ec40c7f77861b4ef3aff708b5a49/librt-0.14.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:079b69b63269ed955fd0c087ac5c24201b522fa1b1ae915b4a489e4ff156ca01", size = 491062, upload-time = "2026-08-06T14:50:41.265Z" }, + { url = "https://files.pythonhosted.org/packages/35/e8/bf985c4e5cc6826f6c6095e2b097b96552665b18d88744816f10f42ad0de/librt-0.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2fd8e75154eb85cf18927fe034c150a6c3fe1c385d1c79d0e5f32ff7ee1f707", size = 522987, upload-time = "2026-08-06T14:50:42.722Z" }, + { url = "https://files.pythonhosted.org/packages/c3/46/f087b1560f840ce0f740f83a8deef2f96e7d21449ade68404a4296f775ed/librt-0.14.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f0a50d043481860050ee965c5c8c89b9f4024023868b9ba152d32a2117bf077", size = 515075, upload-time = "2026-08-06T14:50:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/be/a1/049d31b66a80018e50e315a562d008b8ff0bb3ef84308c47d8e289bcb80d/librt-0.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c1cdf9eeffa6925fc903003cfc9575cf779734c158209df4fabdcfdb4391425", size = 534028, upload-time = "2026-08-06T14:50:45.712Z" }, + { url = "https://files.pythonhosted.org/packages/cd/10/d1b646768500cf969a5ba1b180fa460283043066a559c10d24b9354d2dd6/librt-0.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:371764994ae96aad32cfe3b3d469b430ddd9cf07f7e15ab0cd225d7a4c1a94a3", size = 540547, upload-time = "2026-08-06T14:50:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/5a/91/5992827caf0fc8805592676d47edd013435abd365f76c5a483366afee7db/librt-0.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d6deb7d14a6b0b95c6e9137c6f8071805a9bd07f9a1f50b7a521626c14379ea", size = 523228, upload-time = "2026-08-06T14:50:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/41/2a/337c64452908b11de1ced2b25611d5cffd73e2ada2e9868184cd1a332765/librt-0.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:097cf99296b1a4588dd5108df7c115dee76b07c7484975d36fa4263277ab9d40", size = 565751, upload-time = "2026-08-06T14:50:51.038Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/ea6fe7fe24eeff816a2205994ca5b0e77b405c556cba06080dadafacf8a0/librt-0.14.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a80394c9af4b641a2daed3b45948c05c7022997b8e7fa3e1f104e896c423178e", size = 81612, upload-time = "2026-08-06T14:50:52.617Z" }, + { url = "https://files.pythonhosted.org/packages/c3/31/d9d2fa174db79ccb40e7333ecd5a68ac6eac650baccd405095e3bec77ac7/librt-0.14.0-cp314-cp314-win32.whl", hash = "sha256:254b4e5a894c496f753d42f9d24f1eaffbbbbf39d744849464ab3db0a9408d1e", size = 100111, upload-time = "2026-08-06T14:50:53.91Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/6c6656d9e66b81ff4b6bb6f7e46d16a750fcbcfbdb57db9d8336b8b23ace/librt-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:af5360691ed0f4573bbf1d19aa5f375866c3d7d6259974dab4e156e42c035788", size = 121216, upload-time = "2026-08-06T14:50:55.271Z" }, + { url = "https://files.pythonhosted.org/packages/19/3b/a1abbed7e4cf2969ab71140d3238c9c7a51de73af5251547d3a3be7ca8a1/librt-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4e566a22f852783e6a37a834e6ab5140074581dfc446c76b92e6c87ed81ef459", size = 106395, upload-time = "2026-08-06T14:50:56.603Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9b/2467e569fea8b180001c799c5956e295ca332989e0133dce0c0d03a0bafe/librt-0.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ca165b42e1d5f0fae2f15f6e3ecc27a7bc37f6e81263147233622618335d5453", size = 159534, upload-time = "2026-08-06T14:50:57.879Z" }, + { url = "https://files.pythonhosted.org/packages/06/60/2eba56188f754793a9b1ccdd6cfe2b3b9ebc2d6e786a6801715bb37f5d73/librt-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b023e7201d954a89707ab7df2850f7d022ce4c70fc94dc06582245a3704d6ea", size = 161605, upload-time = "2026-08-06T14:50:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/50/86/4f172345626c740e7a1f467de357be3a1f1c246e6dcae7e44bac91fe581f/librt-0.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b24337d08efcf1b541244d02c396818d5bb35d38399eca13e09b5c5c200ee5f", size = 701752, upload-time = "2026-08-06T14:51:00.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/34/a4db6345633b15f6a154e7933d0df6812cc25a3f98dc7b12f881cce56b47/librt-0.14.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f744cc71a99cd8ba017be77d0d729f76d616c5047fcc9f77a6c1f93fdc2b1c89", size = 682080, upload-time = "2026-08-06T14:51:02.283Z" }, + { url = "https://files.pythonhosted.org/packages/79/cf/93c04412ee704bcf6db6580aa3ed82d05b319cd6ea65cacb64b1874a4005/librt-0.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c469c5f0d5b577ea60e6a8cfe7932cd610e80370d18ecea6df8d2a53b07fa39", size = 722483, upload-time = "2026-08-06T14:51:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/87/e1/1c0ad3901554c9637e8d7cb1afa4c1227323fd5d9307aa4e9d2cf24f032f/librt-0.14.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1c3b5327c425490b45a8cbf81cc3d858d0bece361f712e86fc9940795bf1a05", size = 729657, upload-time = "2026-08-06T14:51:06.099Z" }, + { url = "https://files.pythonhosted.org/packages/b3/69/dcb83a2baa657aaef0cea242fe78c99bad4c6322313587f7b52c3d2fee05/librt-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2ac2a485f64d3fdce7b07ebe6717e1ccfa9b52542049f2148b8532e4905bd199", size = 752808, upload-time = "2026-08-06T14:51:07.826Z" }, + { url = "https://files.pythonhosted.org/packages/b0/01/f07fe2748f75850254d78fe4fc90120903fec1610ffa84d86d85cfbd2025/librt-0.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b12ae9feb16b86d4e648a7abac72b871c63b8465378a8624b639ad4259f469cb", size = 745241, upload-time = "2026-08-06T14:51:09.399Z" }, + { url = "https://files.pythonhosted.org/packages/1d/32/c7a0a4db94805c05c091c6ef073a038f5cc8f17bfac2a5a58fbffa0a30df/librt-0.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cc6f16ce49a756d66d5ab5ef026001de879ded7a5854682de7bf3e0c3a2d9851", size = 727506, upload-time = "2026-08-06T14:51:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/2e/79/6c8867ccff54b87eb2d5ba93bbe2e0a00e08a18c429b1df839ae3434a84b/librt-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7650be33066c5edce26773dd8e4b124eef05878986f404b0c3dcee47f0812073", size = 774306, upload-time = "2026-08-06T14:51:12.568Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/16ab8a5c7c2782b1e54d26369d1373f2f3a37fa616fe391a5a17bc455c4d/librt-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:388ee73e43c1845e32195bca0cff85d0661d0898ff244d1bbb60d9a2e40e44f6", size = 104357, upload-time = "2026-08-06T14:51:14.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f1/260c2593a24cdbf47a6044a78185be51c18b843aca0462c0afa08d5ff5f6/librt-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8dcd1657a9e81cc83bc32bcaee44d71f3ccf492dcbcc26e1d5f2f80bfd474d93", size = 126988, upload-time = "2026-08-06T14:51:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/3e/3649576800fa2509a23dcc658aa60698d759382ecede3a27652e03028394/librt-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2bf5a2a67aded654124e7af37d7e5aa7c589d34e18bdd468cb9c8f509c67eabb", size = 110768, upload-time = "2026-08-06T14:51:16.66Z" }, + { url = "https://files.pythonhosted.org/packages/c9/45/b3d3d5c8bd05fe86fd19c9a1c72a0ba797e39a309b329023f3bdf596e8eb/librt-0.14.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7c458143b671357394070b9ec71011825141a74f7a55f4315bccaa61888a11c1", size = 149813, upload-time = "2026-08-06T14:51:19.269Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/d9a4bbc6eb4186f0c2072e6d189c13e9afeb8efd65084cce6d950f9ad887/librt-0.14.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:611fb701a8c9f7d3f71e3bac5eba5f184a52a5816f1e262a003bb99a19a16f79", size = 154495, upload-time = "2026-08-06T14:51:20.81Z" }, + { url = "https://files.pythonhosted.org/packages/83/66/1979a4f04158635917ce284e8dda4b4bab5957c09437ee44f895750eaf9b/librt-0.14.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f088e040c0409ec2f6ce0b5aeb7ab58afbba1f66c3697d6820cb88cac9f3be0", size = 497485, upload-time = "2026-08-06T14:51:22.35Z" }, + { url = "https://files.pythonhosted.org/packages/0c/24/ee15cf54085c75c8ca4dcccc2a2f1f72e0b733bef28e7b579f21222d5da6/librt-0.14.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:1c51d27c64260ccb24e6861b702100cf2e0a26f37c0344fac22072e8907a52a9", size = 480375, upload-time = "2026-08-06T14:51:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/ec/01/ce8fcbcbc17d6ded94e65e542c490e30eff93e0f9e37308af1f3c5d9cff5/librt-0.14.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f796372a81dc39918c21d67479c4e3716a34b0236efeeec1fd555f453fb8c6f8", size = 525019, upload-time = "2026-08-06T14:51:25.894Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/6552109d0d060b8b7bfe36a79179ab7c6d3afb4a48299b23e8613192f9db/librt-0.14.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7bcbc90ce85729f4c9933b7ab33ec98389d40a69be3e71d2e94ac2e9de6c032", size = 520324, upload-time = "2026-08-06T14:51:27.448Z" }, + { url = "https://files.pythonhosted.org/packages/98/c1/1e27979381b1504b8a2023459df3f9c739199eec9bdcb513f1a9f24f6e2e/librt-0.14.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:af53c610bc5f71c3d2a73c6525cc90d5765ce45f1fac67310da6199cb4c2dd65", size = 537131, upload-time = "2026-08-06T14:51:29.055Z" }, + { url = "https://files.pythonhosted.org/packages/97/6e/fd9401758881a69e2f615185c35659578a9cf033081ea28bdde82eea901a/librt-0.14.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:fbe1226468cf0fd6f9ac5f58fc5eb5171221c51061e6a768285c683016b58a45", size = 527339, upload-time = "2026-08-06T14:51:30.663Z" }, + { url = "https://files.pythonhosted.org/packages/68/9d/aeabab4ad00c43e6fd494749fd7567d5b31b01790b09afea4540974b4536/librt-0.14.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:33230fe017a0528fb403127917c04efd340721858bd8ff3f1beaca8a41c302f3", size = 529624, upload-time = "2026-08-06T14:51:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/c5/26/c607acf7e900bb010e9221ad8b42e2b5433ffff5b9837d19cc9ef6c641d4/librt-0.14.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:492eaa4e1d3640f14916a7ce0a3f2f6acf1dea87b0edc2c382c28b94003c6430", size = 567641, upload-time = "2026-08-06T14:51:33.977Z" }, + { url = "https://files.pythonhosted.org/packages/bc/be/54bbd5cf7b6d8bb95ee7552058ed13d7e27630d8a8844fb7a2b913cc6664/librt-0.14.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:4c955fecca2557ff8daf843537f2d9f874525b23f7e811641e90e48660cd8c0a", size = 81666, upload-time = "2026-08-06T14:51:35.471Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/5f822cb827a3344d9a518b25d1f241d1d551f122fca3bd65fdb2f79c674d/librt-0.14.0-cp315-cp315-win32.whl", hash = "sha256:9238a60060e6a6400fbbb6d70c1d50974c5951eb0eda7433a4b1469a28cc30d4", size = 100047, upload-time = "2026-08-06T14:51:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/12b9efe0c391c363e11ae05f2a5ef17c7ae6f3bd53894a787317b35caa7e/librt-0.14.0-cp315-cp315-win_amd64.whl", hash = "sha256:07f34798827de9a3922ed748ae44cd82292ca7a189dbc480326fa5eb7a2ce18e", size = 121195, upload-time = "2026-08-06T14:51:38.224Z" }, + { url = "https://files.pythonhosted.org/packages/f0/39/eb506c95f04436093cc32b44153e9abebe8077554349aa41cf4d5ace4c01/librt-0.14.0-cp315-cp315-win_arm64.whl", hash = "sha256:f8894a73e9003996a1a03b0d340057775381e9e224c27e9cfd8af47fdd9ed565", size = 106414, upload-time = "2026-08-06T14:51:39.725Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/5c6f78d3d378f51197e9ce6cba90581e7066629730fccc3b76e32da2b5fb/librt-0.14.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:656ec51037ad010cd3388abba2bb73e5633b244ea5365de2afc767a1b3358353", size = 159419, upload-time = "2026-08-06T14:51:41.132Z" }, + { url = "https://files.pythonhosted.org/packages/06/7c/bbd8a856fd906fe3c1485ef7b09f1f93d558984ac2ab7e04aa0b9be7c7ee/librt-0.14.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:7cb23813b4b88d32481c78adaacbe5cc19318b2989a28fba40fb400c529450e8", size = 161688, upload-time = "2026-08-06T14:51:42.552Z" }, + { url = "https://files.pythonhosted.org/packages/31/ee/c388033b154f8eb0749b77b31d4eee8423a5c6e4c7c08c5617e2715b404e/librt-0.14.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbb8f0536a83bbaed37d0f3cf0b5002d1069562de0d24a72b87f2669f58b08b0", size = 710640, upload-time = "2026-08-06T14:51:44.161Z" }, + { url = "https://files.pythonhosted.org/packages/ef/cb/fc317282a406327037a963264dfe9a15c652b8520168b701ebf947292ccc/librt-0.14.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:58f6fd779181d37cae30f3c15a3a454dc5d1082af00111030f03611c0530afbc", size = 679347, upload-time = "2026-08-06T14:51:45.704Z" }, + { url = "https://files.pythonhosted.org/packages/1a/25/e6c84d91ffa923a772def1358bffe7aead7fb6d375455cbf307af6afc4bf/librt-0.14.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6127d8d81544e1aeb5a8fac3c4b713aa221056c2d1235b36b20acd95bd7c869", size = 729771, upload-time = "2026-08-06T14:51:47.471Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/90af116b05e346a8ff7e4463a459e9946a21b1f6bb06e1174a7baaf0a24c/librt-0.14.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22609292248d1b3a7f28d06f9cfd97b447ac9893f3ff935a321d5caf722dbece", size = 742694, upload-time = "2026-08-06T14:51:49.167Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a7/bcbc4ab5469f8209965d1e582a7acb86188cc7fbb5dc15822460ecee80e3/librt-0.14.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:726c499444e440dd7731b0105ad6daaa28c7fb2806f837ad7dca770ac7991d04", size = 763374, upload-time = "2026-08-06T14:51:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d7/38f24ba16854834efd4d8adaffe0f43fc19ef42871b1bc53c6ec73aa37d7/librt-0.14.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:e5fa45343e5327f9e3c365ea24634171cb66733339d224a892aca3257b7e83f7", size = 743212, upload-time = "2026-08-06T14:51:52.24Z" }, + { url = "https://files.pythonhosted.org/packages/9b/db/37cdb2f96348403ef8a830cabc574f816e0697a987a63a7288e10aa479d6/librt-0.14.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c88e8d3c05b41c7578746db8be5202d2daaf7987bfd6f4d2f7ec60e267671f2e", size = 741904, upload-time = "2026-08-06T14:51:53.871Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d1/3487e262aecb743b651614d747732033e99b9bdf23a76e17adb30f36b42e/librt-0.14.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8de3ffad34619b8f0f8cc02ffebea71883fb53b994903c9150f750a6df9165d7", size = 783665, upload-time = "2026-08-06T14:51:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/d2/de/e123239b4806f3cf9324871478bb5a3da9fdf78a4a1eb4618449b737cf96/librt-0.14.0-cp315-cp315t-win32.whl", hash = "sha256:36d0695186da49f4e9323869e50767391772a04c7bacee4ad3d85dd69e671154", size = 104287, upload-time = "2026-08-06T14:51:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/e0/78/b1b1efbe13d8e31859d077aa2ef02719f3a03392cb50aa09fe7c84770940/librt-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:5982d33852bef29eae68bbc62444b793fbac2fe366acf27eaa8051138ddfabdd", size = 126847, upload-time = "2026-08-06T14:51:58.411Z" }, + { url = "https://files.pythonhosted.org/packages/4a/78/fadc5159a101366998ddcc505e43d24610222c3780ce2c47790676d9f44c/librt-0.14.0-cp315-cp315t-win_arm64.whl", hash = "sha256:14fcc7495ee5a0343b33713139a059e35929a4c337cc276774508a9448a4f320", size = 110636, upload-time = "2026-08-06T14:51:59.759Z" }, ] [[package]] From 34a5d9e54ceb49fc1d288f9f0aee3389b58e670e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 14:25:29 -0500 Subject: [PATCH 16/71] Tag v0.1.0a20 (mcp_swap PR targeting, ruff defaults) Freeze the mcp_swap and ruff work from the unreleased section into the dated 0.1.0a20 entry, add its lead paragraph, and open a fresh 0.1.x unreleased placeholder above it. Bump the package version 0.1.0a19 -> 0.1.0a20 across pyproject.toml and __about__.py, and refresh uv.lock. No tool behavior changes here, so the section carries only Documentation and Development entries. MIGRATION is untouched: it has no unreleased heading to retitle, and this release documents no breaking change. --- CHANGES | 4 ++++ pyproject.toml | 2 +- src/libtmux_mcp/__about__.py | 2 +- uv.lock | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index fde47623..388e8d70 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,10 @@ _Notes on upcoming releases will be added here_ +## libtmux-mcp 0.1.0a20 (2026-08-09) + +libtmux-mcp 0.1.0a20 changes no tool behavior. `scripts/mcp_swap.py` gains `use-local --pr N`, which points installed agent CLIs at a pull request without a checkout and verifies the server before it rewrites any configuration, and its edits now preserve unrelated config text, file permissions, and symlink targets. ruff's curated default rule set is enabled behind a `ruff>=0.16.0` floor, taking the project from 351 enabled rules to 565 and fixing what that surfaced, and the CI workflow actions move to their current majors. In the documentation, the dataclass identifying an MCP caller describes each of its fields instead of reaching the API reference as "Alias for field number 0". + ### Documentation #### Caller identity fields are described (#105) diff --git a/pyproject.toml b/pyproject.toml index 12056fa1..036f261a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "libtmux-mcp" -version = "0.1.0a19" +version = "0.1.0a20" description = "MCP server for tmux, powered by libtmux" requires-python = ">=3.10,<4.0" authors = [ diff --git a/src/libtmux_mcp/__about__.py b/src/libtmux_mcp/__about__.py index 24b7c76f..fcf3b6c8 100644 --- a/src/libtmux_mcp/__about__.py +++ b/src/libtmux_mcp/__about__.py @@ -4,7 +4,7 @@ __title__ = "libtmux-mcp" __package_name__ = "libtmux_mcp" -__version__ = "0.1.0a19" +__version__ = "0.1.0a20" __description__ = "MCP server for tmux, powered by libtmux" __author__ = "Tony Narlock" __email__ = "tony@git-pull.com" diff --git a/uv.lock b/uv.lock index 04bdc6ea..38dc8617 100644 --- a/uv.lock +++ b/uv.lock @@ -1316,7 +1316,7 @@ wheels = [ [[package]] name = "libtmux-mcp" -version = "0.1.0a19" +version = "0.1.0a20" source = { editable = "." } dependencies = [ { name = "fastmcp" }, From 8ca4678c9eaca7dad36946ca6428635fcec9a63c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 15:02:22 -0500 Subject: [PATCH 17/71] mcp(refactor[mcp_swap]): Dispatch per-CLI behavior from CLIInfo why: Three things vary per CLI -- the file format, the key path to the server map, and the shape of one entry -- but only the format was recorded on CLIInfo. The other two were spelled as `cli in (...)` membership tuples repeated across get_server, set_server, delete_server and _all_server_specs. Two of those four dispatches end in a bare `else` that falls through to the TOML `mcp_servers` key, so a CLI registered in CLIS but forgotten in one tuple reports "no entry" instead of failing; the other two raise AssertionError, which the caller's (RuntimeError, ValueError, OSError) handler does not catch. what: - Add `container` (key path to the server map) and `dialect` (entry shape) to CLIInfo, both required so a new CLI cannot be added without deciding them - Replace the four membership dispatches with one `_server_map()` accessor that walks the key path and creates intermediates on demand - Extend the non-mapping guard Claude already had to every CLI: a container key holding something other than a table now raises RuntimeError naming the path, rather than a TypeError out of setdefault - Rename `to_json_dict(include_stdio_type=)` to `to_entry_dict(dialect)` and move the TOML table build behind `_as_toml_table()`, so the two writers no longer duplicate the entry shape No behavior change for the six registered CLIs; the existing 123 mcp_swap tests pass unmodified apart from the fixture gaining the two new required fields. --- scripts/mcp_swap.py | 161 +++++++++++++++++++++++++++-------------- tests/test_mcp_swap.py | 28 ++++++- 2 files changed, 133 insertions(+), 56 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 6139b10b..f981d6eb 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -192,6 +192,16 @@ def _xdg_state_home() -> pathlib.Path: # --------------------------------------------------------------------------- +#: Per-entry shape a CLI expects under its server map. ``standard`` is +#: the Claude-Desktop lineage every CLI here started from — scalar +#: ``command``, sibling ``args`` list, optional ``env`` table. +#: ``claude`` is that shape plus an explicit ``type``/``env`` that +#: Claude writes even when empty. Dialects exist because the shape is +#: not implied by the file format: two CLIs sharing ``fmt="json"`` can +#: still disagree about how one entry is spelled. +Dialect = t.Literal["standard", "claude"] + + @dataclasses.dataclass(frozen=True) class CLIInfo: """Static descriptor for a CLI's config file and discovery heuristics.""" @@ -200,6 +210,13 @@ class CLIInfo: binary: str config_path: pathlib.Path fmt: t.Literal["json", "toml"] + #: Key path from the document root down to the mapping of server + #: name -> entry. A path rather than a single key so a CLI that + #: nests deeper needs no new branch in the four functions that + #: read, write, delete and enumerate entries. + container: tuple[str, ...] + #: Entry shape written and read back for this CLI. + dialect: Dialect CLIS: dict[CLIName, CLIInfo] = { @@ -208,36 +225,48 @@ class CLIInfo: binary="claude", config_path=pathlib.Path.home() / ".claude.json", fmt="json", + container=("mcpServers",), + dialect="claude", ), "codex": CLIInfo( name="codex", binary="codex", config_path=pathlib.Path.home() / ".codex" / "config.toml", fmt="toml", + container=("mcp_servers",), + dialect="standard", ), "cursor": CLIInfo( name="cursor", binary="cursor-agent", config_path=pathlib.Path.home() / ".cursor" / "mcp.json", fmt="json", + container=("mcpServers",), + dialect="standard", ), "gemini": CLIInfo( name="gemini", binary="gemini", config_path=pathlib.Path.home() / ".gemini" / "settings.json", fmt="json", + container=("mcpServers",), + dialect="standard", ), "grok": CLIInfo( name="grok", binary="grok", config_path=pathlib.Path.home() / ".grok" / "config.toml", fmt="toml", + container=("mcp_servers",), + dialect="standard", ), "agy": CLIInfo( name="agy", binary="agy", config_path=(pathlib.Path.home() / ".gemini" / "config" / "mcp_config.json"), fmt="json", + container=("mcpServers",), + dialect="standard", ), } @@ -256,11 +285,11 @@ class McpServerSpec: args: list[str] = dataclasses.field(default_factory=list) env: dict[str, str] = dataclasses.field(default_factory=dict) - def to_json_dict(self, *, include_stdio_type: bool = False) -> dict[str, t.Any]: - """Serialize to the JSON shape (Claude-extended when ``include_stdio_type``).""" - # Claude's format always includes ``type`` and ``env`` (even when empty); - # Cursor/Gemini omit both. include_stdio_type selects Claude shape. - if include_stdio_type: + def to_entry_dict(self, dialect: Dialect = "standard") -> dict[str, t.Any]: + """Serialize to the entry shape ``dialect`` expects.""" + # Claude's format always includes ``type`` and ``env`` (even when + # empty); the standard shape omits both when there is nothing to say. + if dialect == "claude": return { "type": "stdio", "command": self.command, @@ -560,6 +589,58 @@ def _claude_user_servers( return existing +def _server_map(info: CLIInfo, config: t.Any, *, create: bool) -> t.Any | None: + """Walk ``info.container`` to the mapping holding this CLI's entries. + + Returns ``None`` when the path is absent and ``create`` is false. + Intermediate levels are created on demand so a nested container needs + no special case; TOML gets tomlkit tables so the written document + keeps its formatting. + + Raises + ------ + RuntimeError + A key along the path holds something other than a mapping. + Reported rather than overwritten — a swap must never discard + config it cannot interpret. + """ + node = config + for depth, key in enumerate(info.container): + child = node.get(key) + if child is None: + if not create: + return None + child = tomlkit.table() if info.fmt == "toml" else {} + node[key] = child + elif not isinstance(child, dict): + path = ".".join(info.container[: depth + 1]) + msg = ( + f"{info.config_path}: {path} is a {type(child).__name__}, " + f"expected a table of server entries" + ) + raise RuntimeError(msg) + node = child + return node + + +def _as_toml_table(entry: dict[str, t.Any]) -> tomlkit.items.Table: + """Render one entry dict as a tomlkit table. + + Nested mappings (``env``) become sub-tables so the written document + keeps TOML's own structure instead of an inline dict literal. + """ + table = tomlkit.table() + for key, value in entry.items(): + if isinstance(value, dict): + sub = tomlkit.table() + for sub_key, sub_value in value.items(): + sub[sub_key] = sub_value + table[key] = sub + else: + table[key] = value + return table + + def get_server( cli: CLIName, config: t.Any, @@ -583,13 +664,12 @@ def get_server( if not node: return None entry = node.get("mcpServers", {}).get(name) - elif cli in ("cursor", "gemini", "agy"): - entry = config.get("mcpServers", {}).get(name) - else: # cli in ("codex", "grok") - entry = config.get("mcp_servers", {}).get(name) + else: + servers = _server_map(CLIS[cli], config, create=False) + entry = servers.get(name) if servers else None if entry is None: return None - return _spec_from_entry(entry, fmt=CLIS[cli].fmt) + return _spec_from_entry(entry, info=CLIS[cli]) def set_server( @@ -613,37 +693,19 @@ def set_server( if scope == "user": servers = _claude_user_servers(config, create=True) had = name in servers - servers[name] = spec.to_json_dict(include_stdio_type=True) + servers[name] = spec.to_entry_dict("claude") return "replaced" if had else "added" node = _claude_project_node(config, repo, create=True) servers = node.setdefault("mcpServers", {}) had = name in servers - servers[name] = spec.to_json_dict(include_stdio_type=True) + servers[name] = spec.to_entry_dict("claude") return "replaced" if had else "added" - if cli in ("cursor", "gemini", "agy"): - servers = config.setdefault("mcpServers", {}) - had = name in servers - servers[name] = spec.to_json_dict() - return "replaced" if had else "added" - if cli in ("codex", "grok"): - # tomlkit: top-level tables are accessed via dict protocol too. - mcp_servers = config.get("mcp_servers") - if mcp_servers is None: - mcp_servers = tomlkit.table() - config["mcp_servers"] = mcp_servers - had = name in mcp_servers - table = tomlkit.table() - table["command"] = spec.command - table["args"] = list(spec.args) - if spec.env: - env_tbl = tomlkit.table() - for k, v in spec.env.items(): - env_tbl[k] = v - table["env"] = env_tbl - mcp_servers[name] = table - return "replaced" if had else "added" - msg = f"unreachable: unknown CLI {cli!r}" - raise AssertionError(msg) + info = CLIS[cli] + servers = _server_map(info, config, create=True) + had = name in servers + entry = spec.to_entry_dict(info.dialect) + servers[name] = _as_toml_table(entry) if info.fmt == "toml" else entry + return "replaced" if had else "added" def delete_server( @@ -671,24 +733,17 @@ def delete_server( return False servers = node.get("mcpServers", {}) return servers.pop(name, None) is not None - if cli in ("cursor", "gemini", "agy"): - return config.get("mcpServers", {}).pop(name, None) is not None - if cli in ("codex", "grok"): - mcp_servers = config.get("mcp_servers") - if mcp_servers is None: - return False - if name in mcp_servers: - del mcp_servers[name] - return True + servers = _server_map(CLIS[cli], config, create=False) + if servers is None or name not in servers: return False - msg = f"unreachable: unknown CLI {cli!r}" - raise AssertionError(msg) + del servers[name] + return True -def _spec_from_entry(entry: t.Any, *, fmt: t.Literal["json", "toml"]) -> McpServerSpec: +def _spec_from_entry(entry: t.Any, *, info: CLIInfo) -> McpServerSpec: """Convert a raw config entry (dict or tomlkit Table) into an McpServerSpec.""" # tomlkit items quack like dicts/lists; coerce to plain Python for our spec. - if fmt == "toml": + if info.fmt == "toml": entry = ( tomlkit.items.Table.unwrap(entry) if isinstance(entry, tomlkit.items.Table) @@ -1555,17 +1610,15 @@ def _add(raw: t.Any) -> None: for name, entry in raw.items(): if not isinstance(entry, dict): continue - out[str(name)] = _spec_from_entry(entry, fmt=CLIS[cli].fmt) + out[str(name)] = _spec_from_entry(entry, info=CLIS[cli]) if cli == "claude": _add(_claude_user_servers(config, create=False)) node = _claude_project_node(config, repo, create=False) if node: _add(node.get("mcpServers")) - elif cli in ("cursor", "gemini", "agy"): - _add(config.get("mcpServers")) - else: # codex, grok - _add(config.get("mcp_servers")) + else: + _add(_server_map(CLIS[cli], config, create=False)) return out diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 5291f636..2698cb58 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -48,36 +48,48 @@ def fake_home(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathli binary="claude", config_path=tmp_path / ".claude.json", fmt="json", + container=("mcpServers",), + dialect="claude", ), "codex": mcp_swap.CLIInfo( name="codex", binary="codex", config_path=tmp_path / ".codex" / "config.toml", fmt="toml", + container=("mcp_servers",), + dialect="standard", ), "cursor": mcp_swap.CLIInfo( name="cursor", binary="cursor-agent", config_path=tmp_path / ".cursor" / "mcp.json", fmt="json", + container=("mcpServers",), + dialect="standard", ), "gemini": mcp_swap.CLIInfo( name="gemini", binary="gemini", config_path=tmp_path / ".gemini" / "settings.json", fmt="json", + container=("mcpServers",), + dialect="standard", ), "grok": mcp_swap.CLIInfo( name="grok", binary="grok", config_path=tmp_path / ".grok" / "config.toml", fmt="toml", + container=("mcp_servers",), + dialect="standard", ), "agy": mcp_swap.CLIInfo( name="agy", binary="agy", config_path=tmp_path / ".gemini" / "config" / "mcp_config.json", fmt="json", + container=("mcpServers",), + dialect="standard", ), }, ) @@ -213,7 +225,14 @@ def test_load_config_tolerates_empty_json(tmp_path: pathlib.Path) -> None: """An empty JSON config can be seeded with the first MCP server entry.""" cfg = tmp_path / "mcp_config.json" cfg.write_text("") - info = mcp_swap.CLIInfo(name="agy", binary="agy", config_path=cfg, fmt="json") + info = mcp_swap.CLIInfo( + name="agy", + binary="agy", + config_path=cfg, + fmt="json", + container=("mcpServers",), + dialect="standard", + ) assert mcp_swap.load_config(info) == {} @@ -2296,7 +2315,12 @@ def _json_config(tmp_path: pathlib.Path, body: str) -> tuple[t.Any, bytes]: raw = body.encode() path.write_bytes(raw) info = mcp_swap.CLIInfo( - name="cursor", binary="cursor-agent", config_path=path, fmt="json" + name="cursor", + binary="cursor-agent", + config_path=path, + fmt="json", + container=("mcpServers",), + dialect="standard", ) return info, raw From 495a3b0987e5daa4e516397936f24cbef0bd072f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 15:15:50 -0500 Subject: [PATCH 18/71] mcp(feat[mcp_swap]): Read and write JSONC without losing comments why: A config format the script cannot round-trip is one it must not write. tomlkit gives TOML a format-preserving round trip; JSON goes through stdlib json.dumps, which reserializes the whole document. For a JSONC file that is doubly wrong -- json.loads rejects `//` outright, and anything that did parse would come back stripped of every comment. The obvious dependency was measured and rejected. json-five round-trips comments via its model API, but it raises on the valid JSON string "C:\\x" and silently decodes the six literal characters \u0041 to "A". stdlib json reads both correctly. A parser that quietly rewrites a value nobody touched is the exact failure this script is built to prevent, so it is not worth a PEP 723 line. what: - Parse JSONC by blanking comments and trailing commas in place -- offsets preserved -- then handing the result to stdlib json, so escape semantics are the standard library's rather than a reimplementation's - Apply writes as text splices located by a string-aware scanner, one splice at a time with a rescan between, so every byte outside a replaced value survives untouched. Same technique opencode's own writer uses through jsonc-parser's modify() - Render short scalar arrays inline so a swapped `command` stays on one line instead of exploding a dotfiles-tracked config into a large diff - Dispatch dump_config_bytes on the exact format instead of `!= "json"`, which would have sent a third format to the TOML writer and put TOML bytes in a JSON file Verified byte-identical round trips for line and block comments, trailing commas, absent final newline, non-ASCII, `//` inside a URL, `/*` inside a string, Windows paths and a literal \u escape. No CLI uses fmt="jsonc" yet; the codec lands ahead of its first consumer. --- scripts/mcp_swap.py | 348 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 345 insertions(+), 3 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index f981d6eb..f91f354e 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -209,7 +209,7 @@ class CLIInfo: name: CLIName binary: str config_path: pathlib.Path - fmt: t.Literal["json", "toml"] + fmt: t.Literal["json", "jsonc", "toml"] #: Key path from the document root down to the mapping of server #: name -> entry. A path rather than a single key so a CLI that #: nests deeper needs no new branch in the four functions that @@ -360,18 +360,348 @@ class SwapStateError(RuntimeError): """Swap state is unsafe to use for a mutating operation.""" +# --------------------------------------------------------------------------- +# JSONC — comments and trailing commas, edited without reserializing +# --------------------------------------------------------------------------- +# +# tomlkit gives TOML a format-preserving round trip; JSONC has no +# equivalent on PyPI that is safe to depend on here. ``json-five`` was +# measured first and rejected: it raises on ``"C:\\x"`` and silently +# decodes the literal six characters ``\u0041`` to ``"A"`` — both valid +# JSON that stdlib reads correctly, and the second is exactly the silent +# rewrite this script exists to avoid. +# +# So values come from stdlib ``json`` (correct escape semantics) and +# edits are applied as text splices located by an offset-preserving +# scanner. Every byte outside a replaced value survives untouched, which +# is the same technique opencode's own config writer uses via +# ``jsonc-parser``'s ``modify()``. + +_JSON_WS = " \t\n\r" + +#: Longest inline rendering of a scalar list before it is broken across +#: lines. A swapped ``command`` array is the common case and reads +#: better on one line, which is how these configs are written by hand. +_INLINE_WIDTH = 88 + + +def _jsonc_blank_comments(text: str) -> str: + """Replace comment bytes with spaces, preserving every offset. + + Scanning rather than matching a regex is the whole point: ``//`` + inside a URL and ``/*`` inside a Windows path are string content, not + comments, and only a scanner that tracks string state can tell them + apart. Offsets are preserved so a span found in the blanked text + addresses the same bytes in the original. + """ + out = list(text) + i, n = 0, len(text) + in_string = False + while i < n: + char = text[i] + if in_string: + if char == "\\": + i += 2 + continue + if char == '"': + in_string = False + i += 1 + elif char == '"': + in_string = True + i += 1 + elif char == "/" and i + 1 < n and text[i + 1] == "/": + while i < n and text[i] != "\n": + out[i] = " " + i += 1 + elif char == "/" and i + 1 < n and text[i + 1] == "*": + end = text.find("*/", i + 2) + end = n if end == -1 else end + 2 + for j in range(i, end): + if out[j] != "\n": + out[j] = " " + i = end + else: + i += 1 + return "".join(out) + + +def _jsonc_blank_trailing_commas(blanked: str) -> str: + """Blank trailing commas so stdlib :func:`json.loads` accepts the text.""" + out = list(blanked) + i, n = 0, len(blanked) + in_string = False + last_comma = -1 + while i < n: + char = blanked[i] + if in_string: + if char == "\\": + i += 2 + continue + if char == '"': + in_string = False + i += 1 + continue + if char == '"': + in_string = True + last_comma = -1 + elif char == ",": + last_comma = i + elif char in "}]": + if last_comma != -1: + out[last_comma] = " " + last_comma = -1 + elif char not in _JSON_WS: + last_comma = -1 + i += 1 + return "".join(out) + + +def _jsonc_loads(text: str) -> t.Any: + """Parse JSONC text into plain Python objects.""" + if not text.strip(): + return {} + return json.loads(_jsonc_blank_trailing_commas(_jsonc_blank_comments(text))) + + +class _JsoncScanner: + """Locate value spans inside comment-blanked JSON text.""" + + def __init__(self, text: str) -> None: + self.text = text + self.pos = 0 + + def skip_ws(self) -> None: + """Advance past insignificant whitespace.""" + while self.pos < len(self.text) and self.text[self.pos] in _JSON_WS: + self.pos += 1 + + def read_string(self) -> str: + """Consume one string token and return its raw text, quotes included.""" + start = self.pos + self.pos += 1 + while self.pos < len(self.text): + char = self.text[self.pos] + if char == "\\": + self.pos += 2 + continue + self.pos += 1 + if char == '"': + break + return self.text[start : self.pos] + + def read_value(self) -> tuple[int, int]: + """Consume one value and return its ``(start, end)`` span.""" + self.skip_ws() + start = self.pos + char = self.text[self.pos] + if char == '"': + self.read_string() + elif char in "{[": + self._read_container() + else: + while ( + self.pos < len(self.text) + and self.text[self.pos] not in ",}]" + and self.text[self.pos] not in _JSON_WS + ): + self.pos += 1 + return start, self.pos + + def _read_container(self) -> None: + self.pos += 1 + depth = 1 + while self.pos < len(self.text) and depth: + char = self.text[self.pos] + if char == '"': + self.read_string() + continue + if char in "{[": + depth += 1 + elif char in "}]": + depth -= 1 + self.pos += 1 + + def read_members(self, obj_start: int) -> list[_JsoncMember]: + """Enumerate an object's members. ``obj_start`` indexes its ``{``.""" + self.pos = obj_start + 1 + found: list[_JsoncMember] = [] + while True: + self.skip_ws() + if self.pos >= len(self.text) or self.text[self.pos] == "}": + return found + if self.text[self.pos] == ",": + self.pos += 1 + continue + member_start = self.pos + raw_key = self.read_string() + self.skip_ws() + self.pos += 1 # the ':' + value_start, value_end = self.read_value() + found.append( + _JsoncMember( + key=json.loads(raw_key), + start=member_start, + end=value_end, + value_start=value_start, + value_end=value_end, + ) + ) + + +class _JsoncMember(t.NamedTuple): + """One ``"key": value`` pair located inside a JSONC document. + + Attributes + ---------- + key : str + The decoded member name. + start : int + Offset of the opening quote of the key. + end : int + Offset just past the value — the end of the whole member. + value_start : int + Offset of the first byte of the value. + value_end : int + Offset just past the last byte of the value. + """ + + key: str + start: int + end: int + value_start: int + value_end: int + + +def _jsonc_render(value: t.Any, depth: int, *, ensure_ascii: bool) -> str: + """Render ``value`` as JSON text indented for nesting ``depth``.""" + pad = " " * depth + if isinstance(value, list) and all( + isinstance(item, (str, int, float, bool)) or item is None for item in value + ): + inline = json.dumps(value, ensure_ascii=ensure_ascii) + if len(inline) + len(pad) <= _INLINE_WIDTH: + return inline + return json.dumps(value, indent=2, ensure_ascii=ensure_ascii).replace( + "\n", "\n" + pad + ) + + +def _jsonc_object_span(blanked: str, path: tuple[str, ...]) -> tuple[int, int] | None: + """Return the span of the object reached by ``path``, or ``None``.""" + scanner = _JsoncScanner(blanked) + scanner.skip_ws() + if scanner.pos >= len(blanked) or blanked[scanner.pos] != "{": + return None + cursor = scanner.pos + for key in path: + match = next( + (m for m in _JsoncScanner(blanked).read_members(cursor) if m.key == key), + None, + ) + if match is None or blanked[match.value_start] != "{": + return None + cursor = match.value_start + tail = _JsoncScanner(blanked) + tail.pos = cursor + return tail.read_value() + + +def _jsonc_next_edit( + text: str, + data: t.Mapping[str, t.Any], + path: tuple[str, ...], + *, + ensure_ascii: bool, +) -> tuple[int, int, str] | None: + """Find the one next splice that brings ``path`` closer to ``data``.""" + blanked = _jsonc_blank_comments(text) + span = _jsonc_object_span(blanked, path) + if span is None: + return None + obj_start, obj_end = span + members = _JsoncScanner(blanked).read_members(obj_start) + by_key = {member.key: member for member in members} + depth = len(path) + 1 + pad = " " * depth + + for key, value in data.items(): + member = by_key.get(key) + if member is None: + body = _jsonc_render(value, depth, ensure_ascii=ensure_ascii) + if members: + tail = members[-1].end + return tail, tail, f',\n{pad}"{key}": {body}' + if blanked[obj_start + 1 : obj_end - 1].strip(): + return None + closing = " " * (depth - 1) + return obj_start + 1, obj_end - 1, f'\n{pad}"{key}": {body}\n{closing}' + current = json.loads( + _jsonc_blank_trailing_commas(blanked[member.value_start : member.value_end]) + ) + if isinstance(value, dict) and isinstance(current, dict): + nested = _jsonc_next_edit( + text, value, (*path, key), ensure_ascii=ensure_ascii + ) + if nested is not None: + return nested + elif current != value: + return ( + member.value_start, + member.value_end, + _jsonc_render(value, depth, ensure_ascii=ensure_ascii), + ) + + for member in members: + if member.key in data: + continue + preceding = max( + (other.end for other in members if other.end <= member.start), + default=obj_start + 1, + ) + trailing = text[member.end : obj_end] + drop_to = member.end + if trailing.lstrip(_JSON_WS).startswith(","): + drop_to += trailing.index(",") + 1 + return preceding, drop_to, "" + return None + + +def _jsonc_merge(text: str, data: t.Mapping[str, t.Any], *, ensure_ascii: bool) -> str: + """Reconcile ``data`` into ``text``, rewriting only members that differ. + + Applies one splice at a time and rescans, so offsets are always + computed against current text rather than patched up after the fact. + Config files are small enough that the extra passes do not matter and + the invariant is worth far more than the cycles. + """ + if not text.strip(): + return json.dumps(dict(data), indent=2, ensure_ascii=ensure_ascii) + "\n" + # One splice per member, plus slack; a config that needs more than + # this has a pathology worth surfacing rather than looping on. + for _ in range(10_000): + edit = _jsonc_next_edit(text, data, (), ensure_ascii=ensure_ascii) + if edit is None: + return text + start, end, replacement = edit + text = text[:start] + replacement + text[end:] + msg = "JSONC merge did not converge" + raise RuntimeError(msg) + + # --------------------------------------------------------------------------- # Config IO — per format # --------------------------------------------------------------------------- def load_config(info: CLIInfo) -> t.Any: - """Parse a CLI's config file (JSON or TOML) into an editable structure. + """Parse a CLI's config file (JSON, JSONC or TOML) into an editable structure. Empty JSON files are treated as empty objects so first-run MCP configs can be seeded with their initial server entry. """ raw = info.config_path.read_bytes() + if info.fmt == "jsonc": + return _jsonc_loads(raw.decode()) if info.fmt == "json": text = raw.decode().strip() return json.loads(text) if text else {} @@ -402,8 +732,20 @@ def dump_config_bytes(info: CLIInfo, config: t.Any, *, original: bytes) -> bytes which is the defect this parameter exists to prevent. tomlkit preserves those conventions itself; only the JSON writer needs it. """ - if info.fmt != "json": + # Dispatched on the exact format rather than "not json": a third + # format reaching the TOML writer by fall-through would silently + # write TOML bytes into a JSON file. + if info.fmt == "toml": return tomlkit.dumps(config).encode() + if info.fmt == "jsonc": + # The merge derives its output from the original text, so the + # file's own trailing-newline convention carries over untouched + # and needs no _json_trailer fixup. + source = original.decode() + try: + return _jsonc_merge(source, config, ensure_ascii=False).encode() + except UnicodeEncodeError: + return _jsonc_merge(source, config, ensure_ascii=True).encode() trailer = _json_trailer(original) # ensure_ascii would re-escape every non-ASCII character in the file, # including config text the swap never read. From e2a0f0cd5b94b4bd384b46f4af41de45cbd25975 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 15:26:05 -0500 Subject: [PATCH 19/71] mcp(feat[mcp_swap]): Support the opencode CLI why: opencode is the seventh agent CLI on this machine and the first whose config differs from the others in all three axes at once: the file is JSONC, the server map hangs off `mcp` rather than `mcpServers`, and one entry packs argv into a single `command` array with its environment table spelled `environment`. Getting any of that wrong is not a soft failure -- a scalar `command` is a decode error that stops opencode from starting at all, and an `env` key is dropped without a word. what: - Register opencode: binary `opencode`, `$XDG_CONFIG_HOME/opencode/ opencode.jsonc` (honouring XDG the way opencode's own loader does), fmt jsonc, container ("mcp",), dialect opencode - Add the opencode dialect to both directions: written as {"type": "local", "command": [argv...]} with "environment", and read back by splitting the array into the portable command/args pair - Seed "$schema" when creating an entry in a config that was empty; opencode writes that line itself on first load, so writing it here avoids a second edit landing right after the swap - Derive the detect column width from the longest registered name instead of a hardcoded 7, which "opencode" overflows Splitting the array on read is what makes `is_local_uv_directory`, `local_repo_path` and `pr_ref` keep working, and those are what the "already local -- no change" check depends on. Without it every run would rewrite a config that was already correct. Verified end to end against a sandboxed HOME/XDG_CONFIG_HOME: add, replace, revert byte-identical, second-run idempotence, a comment living inside the replaced entry, an existing `environment` table, an empty file, a symlinked config, --pr, and status reading each shape back. --- scripts/mcp_swap.py | 94 ++++++++++++++++++++++++++++++++++++------ tests/test_mcp_swap.py | 8 ++++ 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index f91f354e..8eba1529 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -92,8 +92,20 @@ import tomlkit import tomlkit.items -CLIName = t.Literal["claude", "codex", "cursor", "gemini", "grok", "agy"] -ALL_CLIS: tuple[CLIName, ...] = ("claude", "codex", "cursor", "gemini", "grok", "agy") +CLIName = t.Literal["claude", "codex", "cursor", "gemini", "grok", "agy", "opencode"] +ALL_CLIS: tuple[CLIName, ...] = ( + "claude", + "codex", + "cursor", + "gemini", + "grok", + "agy", + "opencode", +) + +#: Width of the CLI-name column in ``detect`` output, derived rather +#: than hardcoded so adding a longer name cannot silently misalign it. +_CLI_COLUMN = max(len(name) for name in ALL_CLIS) + 1 #: Claude config scope: ``"user"`` targets the user/system-level top-level #: ``mcpServers`` fallback that applies to every project without its own @@ -196,10 +208,12 @@ def _xdg_state_home() -> pathlib.Path: #: the Claude-Desktop lineage every CLI here started from — scalar #: ``command``, sibling ``args`` list, optional ``env`` table. #: ``claude`` is that shape plus an explicit ``type``/``env`` that -#: Claude writes even when empty. Dialects exist because the shape is -#: not implied by the file format: two CLIs sharing ``fmt="json"`` can -#: still disagree about how one entry is spelled. -Dialect = t.Literal["standard", "claude"] +#: Claude writes even when empty. ``opencode`` packs argv into a single +#: ``command`` array and spells the environment table ``environment``. +#: Dialects exist because the shape is not implied by the file format: +#: two CLIs sharing ``fmt="json"`` can still disagree about how one +#: entry is spelled. +Dialect = t.Literal["standard", "claude", "opencode"] @dataclasses.dataclass(frozen=True) @@ -268,8 +282,31 @@ class CLIInfo: container=("mcpServers",), dialect="standard", ), + "opencode": CLIInfo( + name="opencode", + binary="opencode", + # opencode reads config.json, opencode.json and opencode.jsonc from + # this directory and merges all three, with .jsonc winning. It writes + # to the first that exists, defaulting to .jsonc — so that is the one + # file a swap can own without being shadowed. + config_path=( + pathlib.Path( + os.environ.get("XDG_CONFIG_HOME") or pathlib.Path.home() / ".config" + ) + / "opencode" + / "opencode.jsonc" + ), + fmt="jsonc", + container=("mcp",), + dialect="opencode", + ), } +#: Written into an opencode config this script creates from nothing. +#: opencode injects the same line itself on first load; seeding it here +#: keeps the swap from being followed by a surprise rewrite. +OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json" + #: A ``--from`` argument pointing at a pull request's head commit. #: GitHub publishes ``refs/pull//head`` on the *base* repository, so @@ -296,7 +333,15 @@ def to_entry_dict(self, dialect: Dialect = "standard") -> dict[str, t.Any]: "args": list(self.args), "env": dict(self.env), } - out: dict[str, t.Any] = {"command": self.command, "args": list(self.args)} + if dialect == "opencode": + # One array for argv, and the table is "environment" -- an + # "env" key here is dropped in silence, and a scalar command + # is a decode error that takes the whole config down with it. + out = {"type": "local", "command": [self.command, *self.args]} + if self.env: + out["environment"] = dict(self.env) + return out + out = {"command": self.command, "args": list(self.args)} if self.env: out["env"] = dict(self.env) return out @@ -1043,6 +1088,10 @@ def set_server( servers[name] = spec.to_entry_dict("claude") return "replaced" if had else "added" info = CLIS[cli] + if info.dialect == "opencode" and not config: + # Seeding from nothing: opencode rewrites the file on load to add + # this line, so writing it now avoids an immediate second edit. + config["$schema"] = OPENCODE_SCHEMA_URL servers = _server_map(info, config, create=True) had = name in servers entry = spec.to_entry_dict(info.dialect) @@ -1083,7 +1132,16 @@ def delete_server( def _spec_from_entry(entry: t.Any, *, info: CLIInfo) -> McpServerSpec: - """Convert a raw config entry (dict or tomlkit Table) into an McpServerSpec.""" + """Convert a raw config entry (dict or tomlkit Table) into an McpServerSpec. + + Every dialect is normalised down to the portable scalar-command + shape, so the helpers that reason about a spec — + :meth:`McpServerSpec.is_local_uv_directory`, :meth:`McpServerSpec.pr_ref`, + ``_points_at`` — stay dialect-agnostic. Skipping this is not a + cosmetic loss: an unsplit array command makes the "already local, no + change" check miss, and every run rewrites a config it did not need + to touch. + """ # tomlkit items quack like dicts/lists; coerce to plain Python for our spec. if info.fmt == "toml": entry = ( @@ -1091,10 +1149,20 @@ def _spec_from_entry(entry: t.Any, *, info: CLIInfo) -> McpServerSpec: if isinstance(entry, tomlkit.items.Table) else dict(entry) ) - command = str(entry.get("command", "")) - raw_args = entry.get("args", []) - args = [str(a) for a in raw_args] if raw_args else [] - raw_env = entry.get("env") or {} + if info.dialect == "opencode": + raw_command = entry.get("command", []) + argv = ( + [str(part) for part in raw_command] + if isinstance(raw_command, (list, tuple)) + else [str(raw_command)] + ) + command, args = (argv[0], argv[1:]) if argv else ("", []) + raw_env = entry.get("environment") or {} + else: + command = str(entry.get("command", "")) + raw_args = entry.get("args", []) + args = [str(a) for a in raw_args] if raw_args else [] + raw_env = entry.get("env") or {} env = {str(k): str(v) for k, v in dict(raw_env).items()} return McpServerSpec(command=command, args=args, env=env) @@ -1440,7 +1508,7 @@ def cmd_detect(args: argparse.Namespace) -> int: if not p.config_found: extra.append(f"config missing: {CLIS[p.cli].config_path}") suffix = f" ({', '.join(extra)})" if extra else "" - print(f" [{flag}] {p.cli:<7}{suffix}") + print(f" [{flag}] {p.cli:<{_CLI_COLUMN}}{suffix}") return 0 diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 2698cb58..33e8213d 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -91,6 +91,14 @@ def fake_home(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathli container=("mcpServers",), dialect="standard", ), + "opencode": mcp_swap.CLIInfo( + name="opencode", + binary="opencode", + config_path=tmp_path / ".config" / "opencode" / "opencode.jsonc", + fmt="jsonc", + container=("mcp",), + dialect="opencode", + ), }, ) state_dir = tmp_path / "state" From 5a9f632644cf26120a401274422d6674e4e9bcc2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 15:32:25 -0500 Subject: [PATCH 20/71] mcp(feat[mcp_swap]): Support the pi CLI, with its adapter caveat stated why: pi is the eighth agent CLI here, and the only one that ships no MCP client. Its README says "No MCP" outright, the released 0.84.1 build contains no MCP code, and its Settings interface has no key that could hold a server. MCP reaches pi only through the third-party `pi-mcp-adapter` extension, which reads ~/.pi/agent/mcp.json in the Claude-Desktop `mcpServers` schema. That leaves one honest way to support pi. This script's value rests on `status` telling the truth about what an agent will actually run, so writing a file pi ignores and reporting success would cost more than not supporting pi at all. Registering the path and naming the missing prerequisite keeps both: the swap lands where the adapter looks, and `detect` says why it will not take effect yet. what: - Register pi: binary `pi`, ~/.pi/agent/mcp.json, fmt json, container ("mcpServers",), standard dialect -- no new dialect needed, the adapter speaks the same shape cursor and gemini do - `detect` appends "needs the pi-mcp-adapter package; pi has no built-in MCP client" whenever that package is absent from ~/.pi/agent/npm/node_modules Verified end to end against a sandboxed HOME: detect's caveat, add, status, and revert byte-identical, with an unrelated server left alone. --- scripts/mcp_swap.py | 29 ++++++++++++++++++++++++++++- tests/test_mcp_swap.py | 8 ++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 8eba1529..029b352a 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -92,7 +92,9 @@ import tomlkit import tomlkit.items -CLIName = t.Literal["claude", "codex", "cursor", "gemini", "grok", "agy", "opencode"] +CLIName = t.Literal[ + "claude", "codex", "cursor", "gemini", "grok", "agy", "opencode", "pi" +] ALL_CLIS: tuple[CLIName, ...] = ( "claude", "codex", @@ -101,6 +103,7 @@ "grok", "agy", "opencode", + "pi", ) #: Width of the CLI-name column in ``detect`` output, derived rather @@ -300,6 +303,16 @@ class CLIInfo: container=("mcp",), dialect="opencode", ), + "pi": CLIInfo( + name="pi", + binary="pi", + # Read by the pi-mcp-adapter extension, not by pi itself; see + # PI_ADAPTER_DIR. Claude-Desktop schema, so the standard dialect. + config_path=pathlib.Path.home() / ".pi" / "agent" / "mcp.json", + fmt="json", + container=("mcpServers",), + dialect="standard", + ), } #: Written into an opencode config this script creates from nothing. @@ -307,6 +320,18 @@ class CLIInfo: #: keeps the swap from being followed by a surprise rewrite. OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json" +#: pi ships no MCP client — its README says "No MCP" outright, and the +#: released build contains no MCP code at all. MCP reaches pi only +#: through the third-party ``pi-mcp-adapter`` extension, which is what +#: reads ``~/.pi/agent/mcp.json``. The swap writes that file because it +#: is the one pi-family location with a settled schema, but until the +#: adapter is installed pi does not read it, so ``detect`` says so +#: instead of reporting a swap that cannot take effect. +PI_ADAPTER_DIR = ( + pathlib.Path.home() / ".pi" / "agent" / "npm" / "node_modules" / "pi-mcp-adapter" +) +PI_ADAPTER_HINT = "needs the pi-mcp-adapter package; pi has no built-in MCP client" + #: A ``--from`` argument pointing at a pull request's head commit. #: GitHub publishes ``refs/pull//head`` on the *base* repository, so @@ -1507,6 +1532,8 @@ def cmd_detect(args: argparse.Namespace) -> int: extra.append("binary missing") if not p.config_found: extra.append(f"config missing: {CLIS[p.cli].config_path}") + if p.cli == "pi" and not PI_ADAPTER_DIR.is_dir(): + extra.append(PI_ADAPTER_HINT) suffix = f" ({', '.join(extra)})" if extra else "" print(f" [{flag}] {p.cli:<{_CLI_COLUMN}}{suffix}") return 0 diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 33e8213d..b0b990bd 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -99,6 +99,14 @@ def fake_home(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathli container=("mcp",), dialect="opencode", ), + "pi": mcp_swap.CLIInfo( + name="pi", + binary="pi", + config_path=tmp_path / ".pi" / "agent" / "mcp.json", + fmt="json", + container=("mcpServers",), + dialect="standard", + ), }, ) state_dir = tmp_path / "state" From 220bedd19c6da25950c61190dcda94c565d189bb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 15:38:42 -0500 Subject: [PATCH 21/71] mcp(test[mcp_swap]): Cover the opencode and pi CLIs why: The two new CLIs introduce axes nothing in the suite exercised: a JSONC config, a container key that is neither mcpServers nor mcp_servers, an entry that packs argv into one array, and a config read by an extension rather than by the agent. The JSONC writer also makes a stronger promise than the JSON one -- it splices text, so it owes byte fidelity rather than only value fidelity, and that has to be asserted on bytes. what: - test_fake_home_covers_every_registered_cli: the fixture replaces CLIS wholesale, so a CLI missing from it raises KeyError out of half a dozen unrelated doctor tests. Names the invariant once - Registration and set/get/delete round-trips for both CLIs, which is what proves each name reached all four container branches - opencode dialect both directions: argv packed into one array, env written as "environment", and the array split back into command+args so is_local_uv_directory, local_repo_path and pr_ref keep working - Comment fidelity: line, block and trailing comments, a comment living inside the entry being replaced, sibling servers, symlinked config, $schema seeding, and a second swap reporting no change - PRESERVED_JSONC byte-identical round-trips, including `//` inside a URL, `/*` inside a string, a Windows path and a literal \u escape -- the cases that make a naive comment-stripper corrupt a value - A parity test asserting JSONC values match stdlib json wherever stdlib can parse the body at all Verified these fail for the right reason: disabling the JSONC writer so jsonc falls through to the plain JSON one turns 8 of them red, the comment and byte-fidelity ones included. --- tests/test_mcp_swap.py | 406 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index b0b990bd..2bc6c81f 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -2984,3 +2984,409 @@ def test_revert_uses_the_original_target_when_a_config_link_is_replaced( assert mcp_swap.cmd_revert(parser.parse_args(["revert", "--cli", "cursor"])) == 0 assert original_target.read_bytes() == original assert replacement_path.read_bytes() == replacement + + +# --------------------------------------------------------------------------- +# opencode and pi +# +# These two exercise axes the first six never did. opencode is the first +# JSONC config, the first container key that is not ``mcpServers`` or +# ``mcp_servers``, and the first entry dialect that packs argv into one +# array; pi is the first CLI whose config is read by an extension rather +# than by the agent itself. The comment-fidelity cases are the point of +# the JSONC codec, so they are asserted on bytes, not on parsed values. +# --------------------------------------------------------------------------- + + +def test_fake_home_covers_every_registered_cli(fake_home: pathlib.Path) -> None: + """``fake_home`` replaces ``CLIS`` wholesale, so it must list every CLI. + + Regression guard rather than a behavior test. ``_config_present_clis`` + iterates ``ALL_CLIS`` while indexing ``CLIS``, so a CLI added to the + registry but not to this fixture raises ``KeyError`` from half a dozen + unrelated doctor and naming-hint tests. Naming the invariant here turns + that into one obvious failure. + """ + assert set(mcp_swap.CLIS) == set(mcp_swap.ALL_CLIS) + + +def test_opencode_and_pi_registered() -> None: + """Both new CLIs are first-class ``--cli`` choices with their own shapes.""" + assert "opencode" in mcp_swap.ALL_CLIS + assert "pi" in mcp_swap.ALL_CLIS + opencode = mcp_swap.CLIS["opencode"] + assert opencode.fmt == "jsonc" + assert opencode.config_path.name == "opencode.jsonc" + assert opencode.container == ("mcp",) + assert opencode.dialect == "opencode" + pi = mcp_swap.CLIS["pi"] + assert pi.fmt == "json" + assert pi.config_path.name == "mcp.json" + assert pi.container == ("mcpServers",) + assert pi.dialect == "standard" + parser = mcp_swap.build_parser() + assert parser.parse_args(["status", "--cli", "opencode"]).cli == ["opencode"] + assert parser.parse_args(["status", "--cli", "pi"]).cli == ["pi"] + + +@pytest.mark.parametrize("cli", ["opencode", "pi"]) +def test_new_cli_set_get_delete_roundtrip(cli: str, fake_repo: pathlib.Path) -> None: + """Each new CLI's four container branches agree with one another. + + Proves the name was threaded through ``get_server``, ``set_server``, + ``delete_server`` and ``_all_server_specs`` rather than falling through + to another CLI's container key. + """ + config: dict[str, t.Any] = {} + spec = mcp_swap.McpServerSpec( + command="uv", args=["--directory", str(fake_repo), "run", "libtmux-mcp"] + ) + assert mcp_swap.set_server(cli, config, "libtmux", spec, fake_repo) == "added" + assert mcp_swap.CLIS[cli].container[0] in config + got = mcp_swap.get_server(cli, config, "libtmux", fake_repo) + assert got is not None + assert got.is_local_uv_directory() + assert got.local_repo_path() == fake_repo + assert mcp_swap.set_server(cli, config, "libtmux", spec, fake_repo) == "replaced" + assert mcp_swap._all_server_specs(cli, config, fake_repo).keys() == {"libtmux"} + assert mcp_swap.delete_server(cli, config, "libtmux", fake_repo) + assert mcp_swap.get_server(cli, config, "libtmux", fake_repo) is None + + +def test_opencode_entry_packs_argv_into_one_command_array( + fake_repo: pathlib.Path, +) -> None: + """The opencode dialect uses one argv array and the key ``environment``. + + A scalar ``command`` is a decode error that stops opencode starting at + all, and an ``env`` key is dropped without a warning, so both spellings + are pinned here rather than left to the round-trip tests. + """ + spec = mcp_swap.McpServerSpec( + command="uv", args=["--directory", "/repo", "run"], env={"A": "b"} + ) + entry = spec.to_entry_dict("opencode") + assert entry["type"] == "local" + assert entry["command"] == ["uv", "--directory", "/repo", "run"] + assert entry["environment"] == {"A": "b"} + assert "args" not in entry + assert "env" not in entry + + +def test_opencode_array_entry_reads_back_as_command_plus_args() -> None: + """An array ``command`` normalizes to the portable scalar-plus-args spec. + + Regression: without the split, ``command`` becomes the ``str()`` of a + Python list, ``is_local_uv_directory`` is False for a correct entry, and + the "already local — no change" short-circuit never fires, so every run + rewrites a config that needed no change. + """ + info = mcp_swap.CLIS["opencode"] + spec = mcp_swap._spec_from_entry( + { + "type": "local", + "command": ["uv", "--directory", "/repo", "run", "libtmux-mcp"], + "environment": {"A": "b"}, + }, + info=info, + ) + assert spec.command == "uv" + assert spec.args == ["--directory", "/repo", "run", "libtmux-mcp"] + assert spec.env == {"A": "b"} + assert spec.is_local_uv_directory() + assert spec.local_repo_path() == pathlib.Path("/repo") + + +def test_opencode_array_entry_round_trips_a_pr_spec() -> None: + """``pr_ref`` still recognises a pull-request spec in the array shape.""" + info = mcp_swap.CLIS["opencode"] + spec = mcp_swap.build_pr_spec( + "https://github.com/tmux-python/libtmux-mcp", 115, "libtmux-mcp" + ) + decoded = mcp_swap._spec_from_entry(spec.to_entry_dict("opencode"), info=info) + assert decoded.pr_ref() == ("https://github.com/tmux-python/libtmux-mcp", 115) + + +def _opencode_config(fake_home: pathlib.Path, body: str) -> t.Any: + """Write ``body`` to the fake opencode config and return its ``CLIInfo``.""" + info = mcp_swap.CLIS["opencode"] + info.config_path.parent.mkdir(parents=True, exist_ok=True) + info.config_path.write_text(body) + return info + + +def _swap_opencode(fake_repo: pathlib.Path) -> int: + """Run ``use-local`` against opencode only.""" + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "opencode"] + ) + return int(mcp_swap.cmd_use_local(args)) + + +def test_opencode_swap_preserves_jsonc_comments( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """Line comments, block comments and sibling servers survive a swap.""" + info = _opencode_config( + fake_home, + "{\n" + " // header comment\n" + ' "$schema": "https://opencode.ai/config.json",\n' + " /* a block comment\n" + " spanning lines */\n" + ' "model": "openrouter/x",\n' + ' "mcp": {\n' + ' "other": { "type": "local", "command": ["echo", "keep"] }\n' + " }\n" + "}\n", + ) + assert _swap_opencode(fake_repo) == 0 + text = info.config_path.read_text() + assert "// header comment" in text + assert "/* a block comment" in text + assert "spanning lines */" in text + doc = mcp_swap._jsonc_loads(text) + assert doc["model"] == "openrouter/x" + assert doc["mcp"]["other"]["command"] == ["echo", "keep"] + assert doc["mcp"]["libtmux"]["command"][0] == "uv" + + +def test_opencode_comment_inside_the_replaced_entry_survives( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """A comment attached to the entry being rewritten is not collateral. + + The case a whole-entry rewrite loses and a field-level splice keeps. + Real opencode configs carry the rationale for a pinned ``command`` + directly above it, which is exactly the text a swap would destroy. + """ + info = _opencode_config( + fake_home, + "{\n" + ' "mcp": {\n' + ' "libtmux": {\n' + ' "type": "local",\n' + " // Pinned deliberately; this rationale must outlive the swap.\n" + ' "command": ["uvx", "libtmux-mcp==0.1.0a2"],\n' + ' "environment": { "KEEP": "me" }\n' + " }\n" + " }\n" + "}\n", + ) + assert _swap_opencode(fake_repo) == 0 + text = info.config_path.read_text() + assert "// Pinned deliberately; this rationale must outlive the swap." in text + entry = mcp_swap._jsonc_loads(text)["mcp"]["libtmux"] + assert entry["command"][0] == "uv" + assert entry["environment"] == {"KEEP": "me"} + + +def test_opencode_swap_and_revert_round_trip_is_byte_identical( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """Revert restores a commented JSONC config byte for byte.""" + body = ( + "{\n" + " // keep me\n" + ' "model": "m",\n' + ' "mcp": {\n' + ' "libtmux": {\n' + ' "type": "local",\n' + ' "command": ["uvx", "libtmux-mcp==0.1.0a2"]\n' + " }\n" + " }\n" + "}\n" + ) + info = _opencode_config(fake_home, body) + original = info.config_path.read_bytes() + assert _swap_opencode(fake_repo) == 0 + assert info.config_path.read_bytes() != original + revert = mcp_swap.build_parser().parse_args(["revert", "--cli", "opencode"]) + assert mcp_swap.cmd_revert(revert) == 0 + assert info.config_path.read_bytes() == original + + +def test_opencode_second_swap_reports_no_change( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """The idempotence check fires for the array command shape. + + Depends on ``_spec_from_entry`` splitting the array; without it the + config is rewritten on every invocation. + """ + info = _opencode_config(fake_home, '{\n "mcp": {}\n}\n') + assert _swap_opencode(fake_repo) == 0 + after_first = info.config_path.read_bytes() + assert _swap_opencode(fake_repo) == 0 + assert info.config_path.read_bytes() == after_first + + +def test_opencode_seeds_schema_into_an_empty_config( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """Seeding an empty file writes ``$schema`` alongside the server entry.""" + info = _opencode_config(fake_home, "") + assert _swap_opencode(fake_repo) == 0 + doc = mcp_swap._jsonc_loads(info.config_path.read_text()) + assert doc["$schema"] == mcp_swap.OPENCODE_SCHEMA_URL + assert doc["mcp"]["libtmux"]["type"] == "local" + + +def test_opencode_symlinked_config_swap_updates_target_not_link( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """A JSONC config symlinked into a dotfiles tree keeps its link.""" + info = mcp_swap.CLIS["opencode"] + target = fake_home / "dotfiles" / "opencode.jsonc" + target.parent.mkdir(parents=True) + target.write_text('{\n // linked\n "mcp": {}\n}\n') + info.config_path.parent.mkdir(parents=True) + info.config_path.symlink_to(target) + + assert _swap_opencode(fake_repo) == 0 + assert info.config_path.is_symlink() + assert info.config_path.readlink() == target + text = target.read_text() + assert "// linked" in text + assert mcp_swap._jsonc_loads(text)["mcp"]["libtmux"]["command"][0] == "uv" + + +def test_detect_reports_the_pi_adapter_prerequisite( + fake_home: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """``detect`` says why a pi swap will not take effect on its own. + + pi ships no MCP client, so the file this script writes is read only by + the ``pi-mcp-adapter`` extension. Reporting pi as swappable without + that caveat would be the one thing this script must never do: claim an + agent will run something it will not. + """ + monkeypatch.setattr(mcp_swap, "PI_ADAPTER_DIR", fake_home / "absent") + monkeypatch.setattr(mcp_swap.shutil, "which", lambda _binary: "/usr/bin/stub") + info = mcp_swap.CLIS["pi"] + info.config_path.parent.mkdir(parents=True) + info.config_path.write_text('{"mcpServers": {}}\n') + + assert mcp_swap.cmd_detect(mcp_swap.build_parser().parse_args(["detect"])) == 0 + out = capsys.readouterr().out + assert mcp_swap.PI_ADAPTER_HINT in out + + monkeypatch.setattr(mcp_swap, "PI_ADAPTER_DIR", fake_home) + assert mcp_swap.cmd_detect(mcp_swap.build_parser().parse_args(["detect"])) == 0 + assert mcp_swap.PI_ADAPTER_HINT not in capsys.readouterr().out + + +# --------------------------------------------------------------------------- +# JSONC writer fidelity +# +# The JSON writer reserializes the whole document, so it can only promise +# to preserve values. The JSONC writer splices text and therefore promises +# bytes: anything it did not deliberately change must come back identical, +# including the comments, the trailing comma, the indent width and the +# absence of a final newline. The string cases exist because a +# comment-stripper that is not string-aware corrupts a URL or a Windows +# path silently, which is the worst failure this codec could have. +# --------------------------------------------------------------------------- + + +PRESERVED_JSONC: list[JSONFidelityCase] = [ + JSONFidelityCase("line_comment", '{\n // note\n "mcp": {}\n}\n'), + JSONFidelityCase("block_comment", '{\n /* note\n more */\n "mcp": {}\n}\n'), + JSONFidelityCase("comment_after_last_member", '{\n "mcp": {}\n // tail\n}\n'), + JSONFidelityCase("trailing_comma", '{\n "mcp": {},\n}\n'), + JSONFidelityCase("no_trailing_newline", '{\n "mcp": {}\n}'), + JSONFidelityCase("four_space_indent", '{\n "mcp": {}\n}\n'), + JSONFidelityCase("url_containing_double_slash", '{\n "a": "https://x/y//z"\n}\n'), + JSONFidelityCase("block_marker_inside_string", '{\n "a": "/* not one */"\n}\n'), + JSONFidelityCase("windows_path", '{\n "a": "C:\\\\tmp\\\\x"\n}\n'), + JSONFidelityCase("literal_backslash_u", '{\n "a": "C:\\\\u0041"\n}\n'), + JSONFidelityCase("emoji_and_cjk", '{\n "a": "🙂 日本語 café"\n}\n'), + JSONFidelityCase("empty_object", "{}\n"), +] + + +def _jsonc_config(tmp_path: pathlib.Path, body: str) -> tuple[t.Any, bytes]: + """Write ``body`` verbatim and return its ``CLIInfo`` and exact bytes.""" + path = tmp_path / "opencode.jsonc" + path.write_text(body) + info = mcp_swap.CLIInfo( + name="opencode", + binary="opencode", + config_path=path, + fmt="jsonc", + container=("mcp",), + dialect="opencode", + ) + return info, path.read_bytes() + + +@pytest.mark.parametrize( + JSONFidelityCase._fields, + PRESERVED_JSONC, + ids=[c.test_id for c in PRESERVED_JSONC], +) +def test_untouched_jsonc_config_round_trips_byte_identical( + test_id: str, body: str, tmp_path: pathlib.Path +) -> None: + """Loading and rewriting an unmodified JSONC config changes no byte.""" + assert test_id + info, raw = _jsonc_config(tmp_path, body) + config = mcp_swap.load_config(info) + assert mcp_swap.dump_config_bytes(info, config, original=raw) == raw + + +@pytest.mark.parametrize( + JSONFidelityCase._fields, + PRESERVED_JSONC, + ids=[c.test_id for c in PRESERVED_JSONC], +) +def test_jsonc_values_match_stdlib_json( + test_id: str, body: str, tmp_path: pathlib.Path +) -> None: + r"""JSONC parsing agrees with stdlib json wherever stdlib can parse. + + Escape handling is the standard library's, not a reimplementation's. + The rejected ``json-five`` dependency failed exactly here: it raised on + ``"C:\\x"`` and decoded a literal ``\\u0041`` to ``"A"``. + """ + assert test_id + try: + expected = json.loads(body) + except json.JSONDecodeError: + pytest.skip("comment or trailing comma — stdlib cannot parse it") + assert mcp_swap._jsonc_loads(body) == expected + + +def test_jsonc_config_is_not_written_through_the_toml_writer( + tmp_path: pathlib.Path, +) -> None: + """A jsonc config comes back as JSON text, not TOML. + + Regression: ``dump_config_bytes`` branched on ``fmt != "json"``, so any + third format reached ``tomlkit.dumps`` and put TOML bytes in a JSON + file. The dispatch is on the exact format now. + """ + info, raw = _jsonc_config(tmp_path, '{\n "mcp": {}\n}\n') + out = mcp_swap.dump_config_bytes( + info, {"mcp": {"x": {"type": "local"}}}, original=raw + ) + text = out.decode() + assert text.lstrip().startswith("{") + assert mcp_swap._jsonc_loads(text)["mcp"]["x"]["type"] == "local" + + +def test_jsonc_comment_blanking_preserves_offsets() -> None: + """Blanking a comment must not move the bytes around it. + + Offsets are what let a span found in the blanked text address the same + bytes in the original; if blanking changed the length, every splice + would land in the wrong place. + """ + src = '{\n // note\n "a": 1, /* x */\n "b": "//not a comment"\n}\n' + blanked = mcp_swap._jsonc_blank_comments(src) + assert len(blanked) == len(src) + assert "//not a comment" in blanked + assert "note" not in blanked + assert blanked.count("\n") == src.count("\n") From 13e2b47366f2e1352530643d5563fb652bdd4979 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 15:50:12 -0500 Subject: [PATCH 22/71] mcp(docs[mcp_swap]): Document opencode and pi across every CLI list why: Eight places enumerate the agent CLIs, and they had already drifted apart before this branch -- scripts/README.md claimed four CLIs when six were supported, and its extension guide named three per-CLI branch sites when there were four. Adding two more CLIs without reconciling them leaves the docs describing a script that no longer exists. what: - Module docstring: line 6 is the argparse description, so it no longer tries to list every CLI by name. The Scope section gains the two new config paths, opencode's three-sibling-global-files caveat, and pi's missing MCP client - scripts/README.md: the CLI table now lists all eight with their formats, and the extension guide describes CLIInfo's fmt/container/ dialect fields instead of branch sites that no longer exist. Adds the ALL_CLIS warning -- a CLI missing from it has its state dropped on load, so revert forgets the swap - docs install widget: an opencode panel. `opencode mcp add tmux -- ` is non-interactive given a name and a `--` command, so it is a CLI panel; that also avoids its array-command shape, which the shared JSON body cannot express. _cli_body falls through to codex by default, so the branch is explicit - Skill and cli-matrix: opencode added to the skill's CLI list and both new CLIs described from source. Their matrix row reads "not yet verified" rather than guessing -- that file's value is that every cell was empirically confirmed, and neither has been driven through the harness - justfile: the mcp-detect comment listed four CLIs; it now names none - CHANGES: entries under Development for the swap-script work, and under Documentation for the install-widget panel pi is deliberately absent from the install widget and has no matrix row: it cannot consume MCP, so there is nothing for a user to install into. --- .../testing-mcp-with-cli-agents/SKILL.md | 15 +++- .../references/cli-matrix.md | 19 ++++++ CHANGES | 53 +++++++++++++++ docs/_ext/widgets/mcp_install.py | 29 ++++++++ justfile | 2 +- scripts/README.md | 68 ++++++++++++++----- scripts/mcp_swap.py | 37 +++++++--- 7 files changed, 193 insertions(+), 30 deletions(-) diff --git a/.agents/skills/testing-mcp-with-cli-agents/SKILL.md b/.agents/skills/testing-mcp-with-cli-agents/SKILL.md index 5a59556c..ab0ca476 100644 --- a/.agents/skills/testing-mcp-with-cli-agents/SKILL.md +++ b/.agents/skills/testing-mcp-with-cli-agents/SKILL.md @@ -2,7 +2,8 @@ name: testing-mcp-with-cli-agents description: >- Test an MCP server by driving real CLI agents (Claude, Codex, Cursor, Gemini, - Grok, agy) against it, using isolated tmux sockets and send-keys instead of + Grok, agy, opencode) against it, using isolated tmux sockets and send-keys + instead of trusting unit tests alone. Use this whenever verifying MCP-server behavior end-to-end, checking that a local branch or checkout works across installed agent CLIs, comparing trunk-vs-branch MCP behavior, driving an interactive @@ -195,7 +196,17 @@ transcripts), and the **ground-truth socket state** after the run. ## Wiring a checkout into the CLIs: mcp_swap `scripts/mcp_swap.py` rewrites each CLI's config to `uv --directory run -` and preserves existing env on replacement: +` and preserves existing env on replacement. It covers eight CLIs; the +two newest are not yet driven through this harness, so +`references/cli-matrix.md` has no verified row for them: + +- **opencode** — `$XDG_CONFIG_HOME/opencode/opencode.jsonc`. JSONC, so + comments survive a swap; the entry packs argv into one `command` array + under a top-level `mcp` key, and its env table is spelled `environment`. + A scalar `command` there is a decode error that stops opencode starting. +- **pi** — `~/.pi/agent/mcp.json`. pi ships no MCP client of its own; that + file is read by the third-party `pi-mcp-adapter` extension, so a swap + does nothing until it is installed. `detect` reports this. ```console $ uv run scripts/mcp_swap.py detect # which CLIs are present diff --git a/.agents/skills/testing-mcp-with-cli-agents/references/cli-matrix.md b/.agents/skills/testing-mcp-with-cli-agents/references/cli-matrix.md index 2c29da9d..b4763dfd 100644 --- a/.agents/skills/testing-mcp-with-cli-agents/references/cli-matrix.md +++ b/.agents/skills/testing-mcp-with-cli-agents/references/cli-matrix.md @@ -75,9 +75,28 @@ below. | gemini | `gemini -p` | project `.gemini/settings.json` from cwd | `gemini mcp list` | `--approval-mode yolo` (`--skip-trust`) | no — `IneligibleTierError`, CLI unsupported for individuals | | grok | `grok -p` / `--single` | `GROK_HOME` **or** `mcp add --scope project` | `grok mcp doctor tmux --json` (real handshake) | `--permission-mode bypassPermissions` | yes | | agy | `agy -p` | hidden `--gemini_dir ` (**credentials do not follow it — copy the token in**) | none short of a model call | `--dangerously-skip-permissions` | yes | +| opencode | not yet verified | not yet verified | not yet verified | not yet verified | not yet driven through this harness | +| pi | n/a — no MCP client (see below) | n/a | n/a | n/a | n/a | ## Per-CLI detail +### opencode and pi — registered in mcp_swap, not yet driven here + +`mcp_swap` writes both, but neither has been taken through the tmux harness, so +the row above is blank rather than guessed. What is known from the source: + +- **opencode** stores MCP servers under a top-level `mcp` key in + `$XDG_CONFIG_HOME/opencode/opencode.jsonc`, as + `{"type": "local", "command": [argv...], "environment": {...}}`. `command` is + one array, not a command/args pair, and the env table is `environment` — an + `env` key is dropped in silence, while a scalar `command` fails the whole + config's decode and stops opencode starting. `opencode mcp add -- ` + is non-interactive once both a name and a `--` command are given. +- **pi** has no MCP client at all: its README says "No MCP", and the released + build contains no MCP code. `~/.pi/agent/mcp.json` is a convention of the + third-party `pi-mcp-adapter` extension. Until that package is installed, + nothing reads what a swap writes, and there is no agent behavior to drive. + ### codex — two isolation styles, both verified - **Config-less (leanest):** a home dir containing only a **copy** of the real `auth.json`, no `config.toml`, plus `-c` overrides: diff --git a/CHANGES b/CHANGES index 388e8d70..b40708cb 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,59 @@ _Notes on upcoming releases will be added here_ +### Documentation + +#### opencode joins the install picker + +The install widget gains an opencode panel. `opencode mcp add tmux -- ` +is non-interactive once a name and a `--` command are both given, so it is a CLI +panel rather than a paste-this-JSON one — which also sidesteps opencode's +unusual entry shape. + +### Development + +**`mcp_swap.py` covers opencode and pi** + +`use-local`, `status`, `revert`, `doctor` and `detect` now reach two more agent +CLIs. + +opencode is the first config the script edits that is not plain JSON or TOML. +Its `$XDG_CONFIG_HOME/opencode/opencode.jsonc` is JSONC, its server map hangs +off a top-level `mcp` key rather than `mcpServers`, and one entry packs argv +into a single `command` array with its environment table spelled +`environment`. Getting the shape wrong is not a soft failure there: a scalar +`command` is a decode error that stops opencode starting, and an `env` key is +dropped without a word. Comments survive a swap, including one written directly +above the `command` it explains. + +pi ships no MCP client — its README says so outright, and the released build +contains no MCP code. `~/.pi/agent/mcp.json` is read by the third-party +`pi-mcp-adapter` extension, so a swap written there takes effect only once that +package is installed. `detect` says so rather than reporting a swap that cannot +do anything. + +**JSONC is edited rather than reserialized** + +The JSON writer rebuilds the whole document, which for a commented file would +mean deleting every comment in it. JSONC values now come from stdlib `json` +after comments and trailing commas are blanked in place, and writes are applied +as text splices, so every byte outside a replaced value is untouched. The +obvious dependency was measured and rejected: `json-five` round-trips comments, +but raises on the valid JSON string `"C:\\x"` and silently decodes a literal +`\u0041` to `"A"`. + +**Per-CLI behavior is declared, not branched** + +`CLIInfo` gained `container` (the key path to the server map) and `dialect` (the +entry shape) alongside `fmt`. The four `cli in (...)` membership tuples that +`get_server`, `set_server`, `delete_server` and `_all_server_specs` each carried +are gone. Two of them ended in a bare `else` that fell through to the TOML key, +so a CLI registered but forgotten in one tuple reported "no entry" instead of +failing; the other two raised `AssertionError`, which the caller did not catch. +A non-mapping at a container key now raises a `RuntimeError` naming the path for +every CLI, not just Claude. `scripts/README.md`'s extension guide described +three branch sites when there were four; it now describes the fields instead. + ## libtmux-mcp 0.1.0a20 (2026-08-09) libtmux-mcp 0.1.0a20 changes no tool behavior. `scripts/mcp_swap.py` gains `use-local --pr N`, which points installed agent CLIs at a pull request without a checkout and verifies the server before it rewrites any configuration, and its edits now preserve unrelated config text, file permissions, and symlink targets. ruff's curated default rule set is enabled behind a `ruff>=0.16.0` floor, taking the project from 351 enabled rules to 565 and fixing what that surfaced, and the CI workflow actions move to their current majors. In the documentation, the dataclass identifying an MCP caller describes each of its fields instead of reaching the API reference as "Alias for field number 0". diff --git a/docs/_ext/widgets/mcp_install.py b/docs/_ext/widgets/mcp_install.py index 39e6e411..d2d5e1b7 100644 --- a/docs/_ext/widgets/mcp_install.py +++ b/docs/_ext/widgets/mcp_install.py @@ -170,6 +170,21 @@ class Panel: ), ) +_OPENCODE_SCOPES: tuple[Scope, ...] = ( + Scope( + id="user", + label="User", + config_file="~/.config/opencode/opencode.jsonc", + note=None, + ), + Scope( + id="project", + label="Project", + config_file="./opencode.json (in repo)", + note=None, + ), +) + _GROK_SCOPES: tuple[Scope, ...] = ( Scope( id="user", @@ -238,6 +253,12 @@ class Panel: kind="json", scopes=_ANTIGRAVITY_SCOPES, ), + Client( + id="opencode", + label="opencode", + kind="cli", + scopes=_OPENCODE_SCOPES, + ), ) @@ -377,6 +398,14 @@ def _cli_body(client: Client, scope: Scope, method: Method, cooldown: Cooldown) # ``--`` is handed to the server process verbatim. if client.id == "grok": return f"grok mcp add --scope {scope.id} tmux -- {tool_cmd}" + # opencode: ``opencode mcp add -- `` is non-interactive as + # soon as a name and a ``--`` command are both given, and it writes + # whichever config file is in scope for the current directory. The + # stored entry packs argv into a single ``command`` array rather than + # the command/args pair the JSON-kind clients use, which is why this + # is a CLI panel and not a paste-this-JSON one. + if client.id == "opencode": + return f"opencode mcp add tmux -- {tool_cmd}" # codex: CLI doesn't write project scope; the project-scope panel # uses the TOML body path (see ``_body_for``). return f"codex mcp add tmux -- {tool_cmd}" diff --git a/justfile b/justfile index 396845b4..a0cec91d 100644 --- a/justfile +++ b/justfile @@ -119,7 +119,7 @@ watch-mypy: format-markdown: prettier --parser=markdown -w *.md docs/*.md docs/**/*.md CHANGES -# Detect which CLI agents (claude/codex/cursor/gemini) exist on this machine +# Detect which agent CLIs exist on this machine [group: 'mcp'] mcp-detect: uv run scripts/mcp_swap.py detect diff --git a/scripts/README.md b/scripts/README.md index 1588f3c0..942d66a0 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -131,14 +131,18 @@ the user-level fallback; the project entry stays. `revert` without ### Scope -Covers four CLIs and their canonical **global** config paths: - -| CLI | Config | Format | -|--------|-------------------------------|--------| -| Claude | `~/.claude.json` | JSON (per-project keying) | -| Codex | `~/.codex/config.toml` | TOML (format-preserving via `tomlkit`) | -| Cursor | `~/.cursor/mcp.json` | JSON | -| Gemini | `~/.gemini/settings.json` | JSON | +Covers eight CLIs and their canonical **global** config paths: + +| CLI | Config | Format | +|-----|--------|--------| +| Claude | `~/.claude.json` | JSON (per-project keying) | +| Codex | `~/.codex/config.toml` | TOML (format-preserving via `tomlkit`) | +| Cursor | `~/.cursor/mcp.json` | JSON | +| Gemini | `~/.gemini/settings.json` | JSON | +| Grok | `~/.grok/config.toml` | TOML (same shape as Codex) | +| agy | `~/.gemini/config/mcp_config.json` | JSON | +| opencode | `$XDG_CONFIG_HOME/opencode/opencode.jsonc` | JSONC (comments preserved) | +| pi | `~/.pi/agent/mcp.json` | JSON (read by `pi-mcp-adapter`, not by pi) | Claude's config is keyed per-project under the repo's absolute path — the script writes only under the current repo's key, leaving other projects' @@ -146,11 +150,21 @@ entries untouched. #### Out of scope (use the CLI's native command) -- **Workspace / project-local configs** for Cursor and Gemini - (`$PWD/.cursor/mcp.json`, `$PWD/.gemini/settings.json`). When - workspace precedence matters, use `cursor mcp add` / `gemini mcp add` - directly — workspace files take precedence over the global ones this - script writes. +- **Workspace / project-local configs** for Cursor, Gemini and opencode + (`$PWD/.cursor/mcp.json`, `$PWD/.gemini/settings.json`, + `$PWD/opencode.json`). When workspace precedence matters, use + `cursor mcp add` / `gemini mcp add` / `opencode mcp add` directly — + workspace files take precedence over the global ones this script + writes. +- **opencode's sibling global files.** opencode merges `config.json`, + `opencode.json` and `opencode.jsonc` from the same directory, with + `.jsonc` winning. This script writes `.jsonc`, so its entry is the one + that takes effect, but a stale `mcp.` in a sibling + `opencode.json` merges underneath rather than being shadowed. +- **pi without `pi-mcp-adapter`.** pi ships no MCP client. The file this + script writes is read by that third-party extension, so until it is + installed the swap has no effect — `detect` reports this rather than + claiming otherwise. - **Custom binary install locations.** Detection is `shutil.which` plus the file existing at the configured global path. Homebrew, npm prefixes (`~/.npm-global/bin`), and the canonical local-install @@ -159,7 +173,27 @@ entries untouched. ### Extending to a new CLI -Add an entry to the `CLIS` table in `mcp_swap.py` and extend the three -per-CLI branches in `get_server` / `set_server` / `delete_server`. Tests -in `tests/test_mcp_swap.py` use a `fake_home` fixture that monkeypatches -`CLIS`, so the extension pattern is already established. +Add an entry to the `CLIS` table in `mcp_swap.py`. Each `CLIInfo` carries +the three things that vary per CLI, so no `get_server` / `set_server` / +`delete_server` / `_all_server_specs` branch needs touching: + +- `fmt` — `json`, `jsonc` or `toml`, selecting the reader and writer +- `container` — the key path to the server map, e.g. `("mcpServers",)` + or `("mcp",)` +- `dialect` — the shape of one entry: `standard` (scalar `command`, + sibling `args`, optional `env`), `claude` (adds `type` and always + writes `env`), or `opencode` (one `command` array, env under + `environment`) + +Add the name to `CLIName` and `ALL_CLIS` too — a CLI in `CLIS` but not +`ALL_CLIS` has its state entries dropped on load, so `revert` forgets the +swap and leaves the config rewritten. + +A dialect no existing CLI speaks needs a branch in +`McpServerSpec.to_entry_dict` and its mirror in `_spec_from_entry`; that +mirror is what keeps `is_local_uv_directory`, `local_repo_path` and +`pr_ref` working, and those drive the "already local" short-circuit. + +Tests in `tests/test_mcp_swap.py` use a `fake_home` fixture that +monkeypatches `CLIS` wholesale, so every new CLI must be added there as +well — `test_fake_home_covers_every_registered_cli` enforces it. diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 029b352a..974aa321 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -3,7 +3,7 @@ # requires-python = ">=3.10" # dependencies = ["tomlkit>=0.13"] # /// -"""Swap MCP server configs across Claude / Codex / Cursor / Gemini / Grok / agy. +"""Swap MCP server configs across every installed agent CLI. Use when you want every installed agent CLI to run a local checkout of an MCP server (editable) instead of a pinned release. ``use-local`` rewrites @@ -37,16 +37,33 @@ - **Global configs only.** Writes to ``~/.cursor/mcp.json``, ``~/.claude.json``, ``~/.codex/config.toml``, ``~/.gemini/settings.json``, ``~/.grok/config.toml`` (TOML - ``mcp_servers``, same shape as Codex), and + ``mcp_servers``, same shape as Codex), ``~/.gemini/config/mcp_config.json`` (agy / Antigravity CLI, JSON ``mcpServers`` — the shared-config file the CLI reads, sibling to the - ``config.json`` it loads at startup). Workspace / project-local configs - (``$PWD/.cursor/mcp.json``, ``$PWD/.gemini/settings.json``, - per-project ``projects..mcpServers`` entries inside - ``~/.claude.json`` *are* recognised for Claude only) are NOT - walked — workspace files for Cursor/Gemini are silently ignored. + ``config.json`` it loads at startup), + ``$XDG_CONFIG_HOME/opencode/opencode.jsonc`` (JSONC ``mcp``, comments + preserved) and ``~/.pi/agent/mcp.json``. Workspace / project-local + configs (``$PWD/.cursor/mcp.json``, ``$PWD/.gemini/settings.json``, + ``$PWD/opencode.json``, per-project ``projects..mcpServers`` + entries inside ``~/.claude.json`` *are* recognised for Claude only) + are NOT walked — workspace files for the others are silently ignored. When workspace precedence matters, run the CLI's own - ``cursor mcp add ...`` / ``gemini mcp add ...`` directly. + ``cursor mcp add ...`` / ``gemini mcp add ...`` / + ``opencode mcp add ...`` directly. + +- **opencode reads three global files.** ``config.json``, + ``opencode.json`` and ``opencode.jsonc`` in the same directory are all + loaded and merged, with ``.jsonc`` winning. This script owns + ``.jsonc`` — the file opencode itself writes to — so its entry is the + one that takes effect. A stale ``mcp.`` left in a sibling + ``opencode.json`` still merges underneath rather than being shadowed + outright; remove it by hand if that matters. + +- **pi has no MCP client of its own.** Its README says so, and the + released build ships no MCP code. ``~/.pi/agent/mcp.json`` is read by + the third-party ``pi-mcp-adapter`` extension, so a swap written there + takes effect only once that package is installed. ``detect`` says as + much rather than reporting a swap that cannot do anything. - **Claude scope.** ``use-local`` and ``revert`` accept ``--scope {user,project}``. The default ``project`` writes the @@ -55,8 +72,8 @@ pre-flag behaviour. ``--scope user`` writes Claude's top-level ``mcpServers`` fallback so every project that has no per-project override picks up the swap; useful when QA-ing a branch across - many directories. Codex, Cursor, Gemini, Grok, and agy have no per-project - layer in their config files; the flag is silently coerced to + many directories. Every other CLI here has no per-project layer in + the config file this script writes; the flag is silently coerced to ``user`` for them. Both Claude scopes can coexist with independent backups; full ``revert`` unwinds in LIFO order. - **Simple binary detection.** Probing is ``shutil.which()`` From 41803f437033acacd8982aea2d8282d731b59d49 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 16:18:49 -0500 Subject: [PATCH 23/71] mcp(fix[mcp_swap]): Satisfy mypy over scripts/ why: CI runs `uv run mypy .`, which covers scripts/; the chain in AGENTS.md is `uv run mypy src tests`, which does not. The opencode work was typed against the narrower invocation and broke the build. what: - Annotate the opencode entry dict, which lost its `dict[str, t.Any]` when the dialect branch was added and was then inferred narrowly enough that assigning `environment` failed - Overload `_server_map` on `create`, matching `_claude_project_node` and `_claude_user_servers`, so a create=True call is not Optional at the call site - Annotate its cursor so the walk returns a mapping rather than Any `just mypy` type-checks every .py file and would have caught this; `uv run mypy src tests` is the invocation that does not. --- scripts/mcp_swap.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 974aa321..20e6240d 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -379,11 +379,14 @@ def to_entry_dict(self, dialect: Dialect = "standard") -> dict[str, t.Any]: # One array for argv, and the table is "environment" -- an # "env" key here is dropped in silence, and a scalar command # is a decode error that takes the whole config down with it. - out = {"type": "local", "command": [self.command, *self.args]} + local: dict[str, t.Any] = { + "type": "local", + "command": [self.command, *self.args], + } if self.env: - out["environment"] = dict(self.env) - return out - out = {"command": self.command, "args": list(self.args)} + local["environment"] = dict(self.env) + return local + out: dict[str, t.Any] = {"command": self.command, "args": list(self.args)} if self.env: out["env"] = dict(self.env) return out @@ -1018,7 +1021,21 @@ def _claude_user_servers( return existing -def _server_map(info: CLIInfo, config: t.Any, *, create: bool) -> t.Any | None: +@t.overload +def _server_map( + info: CLIInfo, config: t.Any, *, create: t.Literal[True] +) -> dict[str, t.Any]: ... + + +@t.overload +def _server_map( + info: CLIInfo, config: t.Any, *, create: t.Literal[False] +) -> dict[str, t.Any] | None: ... + + +def _server_map( + info: CLIInfo, config: t.Any, *, create: bool +) -> dict[str, t.Any] | None: """Walk ``info.container`` to the mapping holding this CLI's entries. Returns ``None`` when the path is absent and ``create`` is false. @@ -1033,7 +1050,7 @@ def _server_map(info: CLIInfo, config: t.Any, *, create: bool) -> t.Any | None: Reported rather than overwritten — a swap must never discard config it cannot interpret. """ - node = config + node: dict[str, t.Any] = config for depth, key in enumerate(info.container): child = node.get(key) if child is None: From fa07d4563d03449ee075792e3b0821af72a4b2a7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 18:42:40 -0500 Subject: [PATCH 24/71] mcp(fix[mcp_swap]): Keep a comment when inserting into an empty object why: The insertion branch asks whether an object already has content by looking at the comment-blanked text, where a comment is indistinguishable from whitespace. An object holding only a comment therefore looked empty, and the insert spliced over the whole interior and took the comment with it -- silently, in a file the user wrote by hand. what: Measure the interior in the original text and anchor the splice after what it actually holds. A genuinely empty interior rstrips to nothing and the anchor collapses to the old splice point, so every previously working insert is byte-identical. Covers the same splice at the document root, where there is no enclosing member, and adds the comment-only object to the byte-fidelity cases. --- scripts/mcp_swap.py | 6 +++++- tests/test_mcp_swap.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 20e6240d..37ba4aa3 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -723,8 +723,12 @@ def _jsonc_next_edit( return tail, tail, f',\n{pad}"{key}": {body}' if blanked[obj_start + 1 : obj_end - 1].strip(): return None + # Blanking hid any comment the object holds, so measure the + # interior in the original text and splice after it, not over it. + interior = text[obj_start + 1 : obj_end - 1] + anchor = obj_start + 1 + len(interior.rstrip()) closing = " " * (depth - 1) - return obj_start + 1, obj_end - 1, f'\n{pad}"{key}": {body}\n{closing}' + return anchor, obj_end - 1, f'\n{pad}"{key}": {body}\n{closing}' current = json.loads( _jsonc_blank_trailing_commas(blanked[member.value_start : member.value_end]) ) diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 2bc6c81f..3e925ea2 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -3304,6 +3304,7 @@ def test_detect_reports_the_pi_adapter_prerequisite( JSONFidelityCase("literal_backslash_u", '{\n "a": "C:\\\\u0041"\n}\n'), JSONFidelityCase("emoji_and_cjk", '{\n "a": "🙂 日本語 café"\n}\n'), JSONFidelityCase("empty_object", "{}\n"), + JSONFidelityCase("comment_only_object", '{\n "mcp": {\n // none yet\n }\n}\n'), ] @@ -3377,6 +3378,46 @@ def test_jsonc_config_is_not_written_through_the_toml_writer( assert mcp_swap._jsonc_loads(text)["mcp"]["x"]["type"] == "local" +def test_jsonc_merge_inserting_into_a_comment_only_object_keeps_the_comment() -> None: + """Regression: blanking made a documented object look empty. + + The emptiness guard reads the comment-blanked text, where a comment is + indistinguishable from whitespace, so insertion used to splice over the + whole interior and take the comment with it. + """ + src = '{\n "mcp": {\n // why there are no servers yet\n }\n}\n' + data = mcp_swap._jsonc_loads(src) + data["mcp"]["tmux"] = {"type": "local", "command": ["uv"]} + out = mcp_swap._jsonc_merge(src, data, ensure_ascii=False) + assert out == ( + '{\n "mcp": {\n // why there are no servers yet\n' + ' "tmux": {\n "type": "local",\n "command": [\n' + ' "uv"\n ]\n }\n }\n}\n' + ) + + +def test_jsonc_merge_inserting_into_a_comment_only_document_keeps_the_comment() -> None: + """The same splice at the root, where there is no enclosing member.""" + src = "{\n // root rationale\n}\n" + data = mcp_swap._jsonc_loads(src) + data["mcp"] = {} + out = mcp_swap._jsonc_merge(src, data, ensure_ascii=False) + assert out == '{\n // root rationale\n "mcp": {}\n}\n' + + +@pytest.mark.parametrize( + "body", + ["{}\n", "{ }\n", '{\n "mcp": {}\n}\n', '{\n "mcp": {\n }\n}\n'], +) +def test_jsonc_merge_inserting_into_an_empty_object_is_unchanged(body: str) -> None: + """A genuinely empty interior still collapses to the old splice point.""" + data = mcp_swap._jsonc_loads(body) + data.setdefault("mcp", {})["tmux"] = {"type": "local"} + out = mcp_swap._jsonc_merge(body, data, ensure_ascii=False) + assert mcp_swap._jsonc_loads(out)["mcp"]["tmux"] == {"type": "local"} + assert out.rstrip().endswith("}") + + def test_jsonc_comment_blanking_preserves_offsets() -> None: """Blanking a comment must not move the bytes around it. From 3d6842ff46a7929f34d3c8fcfa0f75ca594df24b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 18:49:06 -0500 Subject: [PATCH 25/71] mcp(fix[mcp_swap]): Take one delimiter when removing a JSONC member why: Removing a member spliced from the end of the previous member to past the following comma, so a member between two others took the comma on both sides and left its neighbours undelimited. The next merge pass then raised JSONDecodeError, which the caller catches as a bad config, so the swap reported opencode unreadable and skipped it. Reachable without doing anything unusual: an entry carrying `enabled` or `timeout` -- both valid opencode fields the swap does not write -- hits it. what: Take exactly one delimiter with the member. Every member but the first takes the comma before it; the first takes the comma after. Read that comma out of the blanked text, so a comma inside a comment is not mistaken for the separator and a real one behind a comment is still found. Chosen over two larger alternatives after both were built and measured: across 5,508 generated documents this and a helper-based rewrite emitted identical bytes, and a third approach that also preserved the deleted member's comment corrupted files -- it stripped the newline terminating a `//` comment, pulling the closing brace inside it. A comment sitting above a removed member is still removed with it. That is unchanged, and settling it means first deciding whether such a comment documents the member or the object; re-parenting it onto the next member would leave a false statement in the user's file. --- scripts/mcp_swap.py | 16 ++++---- tests/test_mcp_swap.py | 85 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 37ba4aa3..3243d47e 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -745,18 +745,20 @@ def _jsonc_next_edit( _jsonc_render(value, depth, ensure_ascii=ensure_ascii), ) - for member in members: + for index, member in enumerate(members): if member.key in data: continue - preceding = max( - (other.end for other in members if other.end <= member.start), - default=obj_start + 1, - ) - trailing = text[member.end : obj_end] + # Exactly one delimiter leaves with the member: the comma before + # it, or, for the first member which has none, the comma after. + if index: + return members[index - 1].end, member.end, "" + # Read that comma out of the blanked text -- one inside a comment + # is not a delimiter, and a real one behind a comment still is. + trailing = blanked[member.end : obj_end] drop_to = member.end if trailing.lstrip(_JSON_WS).startswith(","): drop_to += trailing.index(",") + 1 - return preceding, drop_to, "" + return obj_start + 1, drop_to, "" return None diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index 3e925ea2..fafe0f9f 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -3305,6 +3305,9 @@ def test_detect_reports_the_pi_adapter_prerequisite( JSONFidelityCase("emoji_and_cjk", '{\n "a": "🙂 日本語 café"\n}\n'), JSONFidelityCase("empty_object", "{}\n"), JSONFidelityCase("comment_only_object", '{\n "mcp": {\n // none yet\n }\n}\n'), + JSONFidelityCase( + "comment_before_the_delimiter", '{\n "a": 1 /* x */,\n "b": 2\n}\n' + ), ] @@ -3378,6 +3381,88 @@ def test_jsonc_config_is_not_written_through_the_toml_writer( assert mcp_swap._jsonc_loads(text)["mcp"]["x"]["type"] == "local" +class JsoncDeletionCase(t.NamedTuple): + """A member removal whose exact resulting text is pinned. + + Attributes + ---------- + test_id : str + Identifier shown in the parametrized test name. + body : str + The config text before the merge. + data : dict[str, t.Any] + The reconciled data the merge is driven with. + expected : str + The exact text the merge must produce. + """ + + test_id: str + body: str + data: dict[str, t.Any] + expected: str + + +JSONC_DELETIONS: list[JsoncDeletionCase] = [ + JsoncDeletionCase( + "first_member", + '{\n "a": 1,\n "b": 2\n}\n', + {"b": 2}, + '{\n "b": 2\n}\n', + ), + JsoncDeletionCase( + "middle_member", + '{\n "a": 1,\n "b": 2,\n "c": 3\n}\n', + {"a": 1, "c": 3}, + '{\n "a": 1,\n "c": 3\n}\n', + ), + JsoncDeletionCase( + "last_member", + '{\n "a": 1,\n "b": 2\n}\n', + {"a": 1}, + '{\n "a": 1\n}\n', + ), + JsoncDeletionCase( + "comma_hidden_behind_a_comment", + '{\n "a": 1 /* x, y */,\n "b": 2\n}\n', + {"b": 2}, + '{\n "b": 2\n}\n', + ), +] + + +@pytest.mark.parametrize( + JsoncDeletionCase._fields, + JSONC_DELETIONS, + ids=[c.test_id for c in JSONC_DELETIONS], +) +def test_jsonc_merge_removing_a_member_takes_exactly_one_comma( + test_id: str, body: str, data: dict[str, t.Any], expected: str +) -> None: + """Regression: a removal took the comma on both sides of the member. + + Deleting a member between two others left its neighbours undelimited, + so the next merge pass raised ``JSONDecodeError`` and the swap reported + the config unreadable. ``comma_hidden_behind_a_comment`` covers the + partner defect: the delimiter scan read the raw text, where a comma + inside a comment passes for the separator. + """ + assert test_id + assert mcp_swap._jsonc_merge(body, data, ensure_ascii=False) == expected + + +def test_jsonc_merge_removing_a_middle_member_stays_parseable() -> None: + """The shape that surfaced it: an opencode entry losing optional fields.""" + src = ( + '{\n "mcp": {\n "tmux": {\n "type": "local",\n' + ' "enabled": true,\n "timeout": 5000,\n' + ' "command": ["uvx", "old"]\n }\n }\n}\n' + ) + data = mcp_swap._jsonc_loads(src) + data["mcp"]["tmux"] = {"type": "local", "command": ["uv", "run", "x"]} + out = mcp_swap._jsonc_merge(src, data, ensure_ascii=False) + assert mcp_swap._jsonc_loads(out) == data + + def test_jsonc_merge_inserting_into_a_comment_only_object_keeps_the_comment() -> None: """Regression: blanking made a documented object look empty. From b01160a8f1c8c6d7fb3237e178ac3e33a290ad15 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 18:53:25 -0500 Subject: [PATCH 26/71] mcp(fix[mcp_swap]): Read pi's config as JSONC why: pi's MCP file is read by pi-mcp-adapter, which parses it through strip-json-comments with trailing commas allowed. Registering it as fmt="json" sent it to strict json.loads, so a config the adapter reads without complaint came back as a JSONDecodeError and status and use-local reported pi unreadable and skipped it. The .json suffix is misleading; the format the reader accepts is JSONC. what: fmt="jsonc". The container key and entry dialect are unchanged -- the adapter speaks the same Claude-Desktop mcpServers shape cursor and gemini do. Comments and a trailing comma now survive a swap as well. --- scripts/mcp_swap.py | 4 +++- tests/test_mcp_swap.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 3243d47e..089a3c67 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -325,8 +325,10 @@ class CLIInfo: binary="pi", # Read by the pi-mcp-adapter extension, not by pi itself; see # PI_ADAPTER_DIR. Claude-Desktop schema, so the standard dialect. + # The adapter parses through strip-json-comments with trailing + # commas allowed, so the file is JSONC despite the .json suffix. config_path=pathlib.Path.home() / ".pi" / "agent" / "mcp.json", - fmt="json", + fmt="jsonc", container=("mcpServers",), dialect="standard", ), diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index fafe0f9f..b0a12159 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -103,7 +103,7 @@ def fake_home(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathli name="pi", binary="pi", config_path=tmp_path / ".pi" / "agent" / "mcp.json", - fmt="json", + fmt="jsonc", container=("mcpServers",), dialect="standard", ), @@ -3020,7 +3020,7 @@ def test_opencode_and_pi_registered() -> None: assert opencode.container == ("mcp",) assert opencode.dialect == "opencode" pi = mcp_swap.CLIS["pi"] - assert pi.fmt == "json" + assert pi.fmt == "jsonc" assert pi.config_path.name == "mcp.json" assert pi.container == ("mcpServers",) assert pi.dialect == "standard" @@ -3251,6 +3251,32 @@ def test_opencode_symlinked_config_swap_updates_target_not_link( assert mcp_swap._jsonc_loads(text)["mcp"]["libtmux"]["command"][0] == "uv" +def test_pi_config_with_comments_is_readable( + fake_home: pathlib.Path, fake_repo: pathlib.Path +) -> None: + """Regression: pi's adapter accepts JSONC, so strict JSON rejected it. + + ``pi-mcp-adapter`` reads the file through ``strip-json-comments`` with + trailing commas allowed. Parsing it as strict JSON made ``status`` and + ``use-local`` report a config the adapter reads fine as unreadable. + """ + info = mcp_swap.CLIS["pi"] + info.config_path.parent.mkdir(parents=True) + info.config_path.write_text( + '{\n // the adapter allows comments\n "mcpServers": {\n' + ' "keep": { "command": "echo", "args": ["hi"] },\n }\n}\n' + ) + args = mcp_swap.build_parser().parse_args( + ["use-local", "--repo", str(fake_repo), "--cli", "pi"] + ) + assert mcp_swap.cmd_use_local(args) == 0 + text = info.config_path.read_text() + assert "// the adapter allows comments" in text + servers = mcp_swap._jsonc_loads(text)["mcpServers"] + assert servers["keep"]["command"] == "echo" + assert servers["libtmux"]["command"] == "uv" + + def test_detect_reports_the_pi_adapter_prerequisite( fake_home: pathlib.Path, monkeypatch: pytest.MonkeyPatch, From 4f22f7d04a0f4d9317bc20c82b2cd3232a150079 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 18:58:39 -0500 Subject: [PATCH 27/71] mcp(fix[mcp_install]): Drop opencode's project scope from the picker why: The panel offered Project alongside User and named `./opencode.json` as its destination, but emitted the same command for both. `opencode mcp add` resolves its target with resolveConfigPath(Global.Path.config, true) on the non-interactive path, so it writes the global file whichever scope was picked. A reader following the Project panel would register the server for every project while believing it was scoped to one repo. what: opencode offers User only. The prose that pointed at `opencode mcp add` for workspace precedence is corrected in the same pass -- that command cannot reach a project file; editing `$PWD/opencode.json` by hand can. --- docs/_ext/widgets/mcp_install.py | 9 +++------ scripts/README.md | 7 ++++--- scripts/mcp_swap.py | 5 +++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/_ext/widgets/mcp_install.py b/docs/_ext/widgets/mcp_install.py index d2d5e1b7..88b8a867 100644 --- a/docs/_ext/widgets/mcp_install.py +++ b/docs/_ext/widgets/mcp_install.py @@ -170,6 +170,9 @@ class Panel: ), ) +#: User scope only: `opencode mcp add` writes the global config whether or +#: not a project one exists, so a Project panel would advertise a file the +#: command it prints never touches. _OPENCODE_SCOPES: tuple[Scope, ...] = ( Scope( id="user", @@ -177,12 +180,6 @@ class Panel: config_file="~/.config/opencode/opencode.jsonc", note=None, ), - Scope( - id="project", - label="Project", - config_file="./opencode.json (in repo)", - note=None, - ), ) _GROK_SCOPES: tuple[Scope, ...] = ( diff --git a/scripts/README.md b/scripts/README.md index 942d66a0..35727e7b 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -153,9 +153,10 @@ entries untouched. - **Workspace / project-local configs** for Cursor, Gemini and opencode (`$PWD/.cursor/mcp.json`, `$PWD/.gemini/settings.json`, `$PWD/opencode.json`). When workspace precedence matters, use - `cursor mcp add` / `gemini mcp add` / `opencode mcp add` directly — - workspace files take precedence over the global ones this script - writes. + `cursor mcp add` / `gemini mcp add` directly — workspace files take + precedence over the global ones this script writes. opencode has no + non-interactive project-scope add (`opencode mcp add` writes the global + file), so edit `$PWD/opencode.json` by hand. - **opencode's sibling global files.** opencode merges `config.json`, `opencode.json` and `opencode.jsonc` from the same directory, with `.jsonc` winning. This script writes `.jsonc`, so its entry is the one diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 089a3c67..e753ba50 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -48,8 +48,9 @@ entries inside ``~/.claude.json`` *are* recognised for Claude only) are NOT walked — workspace files for the others are silently ignored. When workspace precedence matters, run the CLI's own - ``cursor mcp add ...`` / ``gemini mcp add ...`` / - ``opencode mcp add ...`` directly. + ``cursor mcp add ...`` / ``gemini mcp add ...`` directly. opencode has + no non-interactive project-scope add -- ``opencode mcp add`` writes the + global file -- so edit ``$PWD/opencode.json`` by hand for that. - **opencode reads three global files.** ``config.json``, ``opencode.json`` and ``opencode.jsonc`` in the same directory are all From b87be825d8bd87baadf02ad81819444bf62f08f4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 19:02:57 -0500 Subject: [PATCH 28/71] mcp(docs[mcp_swap]): Record pi's config as JSONC The CLI table and the scope note still called it JSON, which is what the suffix says and not what the adapter reading it accepts. --- scripts/README.md | 2 +- scripts/mcp_swap.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index 35727e7b..21f07969 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -142,7 +142,7 @@ Covers eight CLIs and their canonical **global** config paths: | Grok | `~/.grok/config.toml` | TOML (same shape as Codex) | | agy | `~/.gemini/config/mcp_config.json` | JSON | | opencode | `$XDG_CONFIG_HOME/opencode/opencode.jsonc` | JSONC (comments preserved) | -| pi | `~/.pi/agent/mcp.json` | JSON (read by `pi-mcp-adapter`, not by pi) | +| pi | `~/.pi/agent/mcp.json` | JSONC (read by `pi-mcp-adapter`, not by pi) | Claude's config is keyed per-project under the repo's absolute path — the script writes only under the current repo's key, leaving other projects' diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index e753ba50..520fc224 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -42,7 +42,8 @@ ``mcpServers`` — the shared-config file the CLI reads, sibling to the ``config.json`` it loads at startup), ``$XDG_CONFIG_HOME/opencode/opencode.jsonc`` (JSONC ``mcp``, comments - preserved) and ``~/.pi/agent/mcp.json``. Workspace / project-local + preserved) and ``~/.pi/agent/mcp.json`` (JSONC too -- the adapter that + reads it strips comments). Workspace / project-local configs (``$PWD/.cursor/mcp.json``, ``$PWD/.gemini/settings.json``, ``$PWD/opencode.json``, per-project ``projects..mcpServers`` entries inside ``~/.claude.json`` *are* recognised for Claude only) From efa1e08c371a41b049f0c29a0b37f43cc4f81219 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 20:09:46 -0500 Subject: [PATCH 29/71] mcp(fix[mcp_swap]): Escape a key inserted into a JSONC config why: The insertion path built the member with an f-string, so the key went in raw while every value went through json.dumps. `--server` takes an arbitrary string: give it one holding a backslash, a quote, or a newline and the emitted text does not parse back. The member is then never found on the next pass, so the merge re-inserts it until the pass ceiling -- burning CPU for over an hour while holding the exclusive swap lock, then failing with "JSONC merge did not converge". what: Render the key with json.dumps, honouring the same ensure_ascii the values use. Found by exercising the flag surface rather than the config surface; the config-shape matrix passes either way because a derived server name never contains one of these characters. --- scripts/mcp_swap.py | 9 +++++++-- tests/test_mcp_swap.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 520fc224..20b61821 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -722,9 +722,14 @@ def _jsonc_next_edit( member = by_key.get(key) if member is None: body = _jsonc_render(value, depth, ensure_ascii=ensure_ascii) + # Escape the key like any other value: written raw, a backslash + # or quote in a server name emits text that cannot be parsed + # back, so the member is never found and the merge re-inserts + # it until the pass ceiling, holding the swap lock throughout. + name = json.dumps(key, ensure_ascii=ensure_ascii) if members: tail = members[-1].end - return tail, tail, f',\n{pad}"{key}": {body}' + return tail, tail, f",\n{pad}{name}: {body}" if blanked[obj_start + 1 : obj_end - 1].strip(): return None # Blanking hid any comment the object holds, so measure the @@ -732,7 +737,7 @@ def _jsonc_next_edit( interior = text[obj_start + 1 : obj_end - 1] anchor = obj_start + 1 + len(interior.rstrip()) closing = " " * (depth - 1) - return anchor, obj_end - 1, f'\n{pad}"{key}": {body}\n{closing}' + return anchor, obj_end - 1, f"\n{pad}{name}: {body}\n{closing}" current = json.loads( _jsonc_blank_trailing_commas(blanked[member.value_start : member.value_end]) ) diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index b0a12159..c7931ae4 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -3476,6 +3476,24 @@ def test_jsonc_merge_removing_a_member_takes_exactly_one_comma( assert mcp_swap._jsonc_merge(body, data, ensure_ascii=False) == expected +@pytest.mark.parametrize( + "name", ["back\\slash", 'quo"te', "new\nline", "tab\tbed", "unicode\u00e9"] +) +def test_jsonc_merge_escapes_an_inserted_key(name: str) -> None: + """Regression: an inserted key was written raw, so a swap could not converge. + + ``--server`` takes an arbitrary string. Written unescaped, a backslash or + quote in it emitted text that would not parse back, so the member was + never found again and the merge re-inserted it until the pass ceiling -- + spinning while holding the swap lock and then failing. + """ + src = '{\n "mcp": {}\n}\n' + data = mcp_swap._jsonc_loads(src) + data["mcp"][name] = {"type": "local"} + out = mcp_swap._jsonc_merge(src, data, ensure_ascii=False) + assert mcp_swap._jsonc_loads(out)["mcp"][name] == {"type": "local"} + + def test_jsonc_merge_removing_a_middle_member_stays_parseable() -> None: """The shape that surfaced it: an opencode entry losing optional fields.""" src = ( From e0433d01a1d69de19f220cca4f6d266471e7a042 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 20:20:37 -0500 Subject: [PATCH 30/71] mcp(fix[mcp_swap]): Ignore a relative XDG_CONFIG_HOME why: opencode's config path was taken from $XDG_CONFIG_HOME verbatim. A relative value resolves against the working directory, so the swap read one file when run from one directory and another from elsewhere, and the backup path recorded in the state file was relative too. Revert from any other directory then reported the backup missing, and because a missing backup leaves its state entry in place, that CLI stayed wedged. what: Fall back to ~/.config unless the variable is absolute, which is what the XDG spec requires -- absolute or ignored. --- scripts/mcp_swap.py | 22 +++++++++++++++------- tests/test_mcp_swap.py | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 20b61821..78428a08 100755 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -255,6 +255,20 @@ class CLIInfo: dialect: Dialect +def _xdg_config_home() -> pathlib.Path: + """``$XDG_CONFIG_HOME`` when absolute, else ``~/.config``. + + The spec requires these variables to be absolute and says to ignore + them otherwise. A relative value would resolve against the working + directory, so the swap would record a backup path that revert could + no longer find from anywhere else. + """ + raw = os.environ.get("XDG_CONFIG_HOME") + if raw and pathlib.Path(raw).is_absolute(): + return pathlib.Path(raw) + return pathlib.Path.home() / ".config" + + CLIS: dict[CLIName, CLIInfo] = { "claude": CLIInfo( name="claude", @@ -311,13 +325,7 @@ class CLIInfo: # this directory and merges all three, with .jsonc winning. It writes # to the first that exists, defaulting to .jsonc — so that is the one # file a swap can own without being shadowed. - config_path=( - pathlib.Path( - os.environ.get("XDG_CONFIG_HOME") or pathlib.Path.home() / ".config" - ) - / "opencode" - / "opencode.jsonc" - ), + config_path=_xdg_config_home() / "opencode" / "opencode.jsonc", fmt="jsonc", container=("mcp",), dialect="opencode", diff --git a/tests/test_mcp_swap.py b/tests/test_mcp_swap.py index c7931ae4..bfac89d0 100644 --- a/tests/test_mcp_swap.py +++ b/tests/test_mcp_swap.py @@ -3010,6 +3010,29 @@ def test_fake_home_covers_every_registered_cli(fake_home: pathlib.Path) -> None: assert set(mcp_swap.CLIS) == set(mcp_swap.ALL_CLIS) +@pytest.mark.parametrize("raw", ["relcfg", "", " ", "./cfg"]) +def test_relative_xdg_config_home_is_ignored( + raw: str, monkeypatch: pytest.MonkeyPatch +) -> None: + """Regression: a relative XDG_CONFIG_HOME resolved against the cwd. + + The spec requires these to be absolute and to be ignored otherwise. + Honouring a relative one made opencode's config -- and the backup path + recorded for it -- depend on where the swap was run from, so revert + from any other directory reported the backup missing for good. + """ + monkeypatch.setenv("XDG_CONFIG_HOME", raw) + assert mcp_swap._xdg_config_home() == pathlib.Path.home() / ".config" + + +def test_absolute_xdg_config_home_is_honoured( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Opencode resolves XDG the way its own loader does.""" + monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path)) + assert mcp_swap._xdg_config_home() == tmp_path + + def test_opencode_and_pi_registered() -> None: """Both new CLIs are first-class ``--cli`` choices with their own shapes.""" assert "opencode" in mcp_swap.ALL_CLIS From 1c0e3df8373900e7cd5d712a3aa754145fa07076 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 14 Aug 2026 17:49:11 -0500 Subject: [PATCH 31/71] ai(rules[AGENTS]) Unify code block rules why: Code block guidance drifted into four variants across repos. what: - Merge the code block and shell command sections - Lead with the paste-and-run contract --- AGENTS.md | 52 ++++++++++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 441dd724..e44391e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -416,46 +416,38 @@ mention can carry the link. Leave command examples, code blocks, Mermaid node labels, and literal configuration values as code; link the surrounding prose instead. -### Code Blocks in Documentation - -When writing documentation (README, CHANGES, docs/), follow these rules for code blocks: - -**One command per code block.** This makes commands individually copyable. For sequential commands, either use separate code blocks or chain them with `&&` or `;` and `\` continuations (keeping it one logical command). - -**Put explanations outside the code block**, not as comments inside. - -### Shell Command Formatting - -**Use `console` language tag with `$ ` prefix.** +### Code Blocks + +Code blocks are paste-and-run units: pasting one block runs exactly one +intended action. Doctests and other executed examples are exempt — the test +suite runs them, nobody pastes them. + +- **One command per block.** Multiple steps may share a block only when + explicitly chained with `&&`, `;`, or `\` continuations — the chain is + then one logical command. +- **Explanations go in prose above the block**, never as `#` comments inside it. +- **Command menus are per-command blocks with prose lead-ins**, not tables. +- **Shell commands use the `console` tag with a `$ ` prefix.** This separates + interactive commands from scripts and enables prompt-aware copy. +- **Split long commands with `\`** — one flag or flag+value pair per indented + continuation line, positional arguments last. Good: -```console -$ uv run pytest -``` - -Bad: - -```bash -uv run pytest -``` - -**Split long commands with `\` for readability.** Each flag or flag+value pair gets its own continuation line, indented. Positional parameters go on the final line. - -Good: +Show the last ten commits as a graph: ```console -$ claude mcp add \ - --scope user \ - tmux -- \ - uv --directory ~/work/python/libtmux-mcp \ - run libtmux-mcp +$ git log \ + --max-count=10 \ + --graph \ + --oneline ``` Bad: ```console -$ claude mcp add --scope user tmux -- uv --directory ~/work/python/libtmux-mcp run libtmux-mcp +# Show the last ten commits as a graph +$ git log --max-count=10 --graph --oneline ``` ### Changelog Conventions From b90c58b6278b1ecb7c114955ec44c2de6c33bc8b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 15 Aug 2026 05:25:43 -0500 Subject: [PATCH 32/71] py(deps[dev]) Bump dev packages --- uv.lock | 444 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 230 insertions(+), 214 deletions(-) diff --git a/uv.lock b/uv.lock index 38dc8617..f9912b58 100644 --- a/uv.lock +++ b/uv.lock @@ -114,66 +114,66 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/6a/a68c7f823e67714393060a0f232b0d75e0b2d2f1a7aef0633f7007411804/ast_serialize-0.7.0.tar.gz", hash = "sha256:934c0920454381b0beb46a5dc0af114d48699a44537b97282c2346a23990713d", size = 845507, upload-time = "2026-08-06T14:07:38.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/15/3b0e4edec45fd31a55243e37263236946cdfe6901e3f8cff934a443eddc0/ast_serialize-0.7.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:55e524f1329e2ee3f17d497b45d16a069afd2020b324ef6f32d21831e0b47a81", size = 1177653, upload-time = "2026-08-06T14:06:07.738Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3d/ecb5f748a3ff50153b5956200a8eea265797de98ebe7b6ca9f8f28d65f08/ast_serialize-0.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8dcbb7b94e5cb8c718f4f5c310b608fa73cd3714b819fe1995b20acfa6847689", size = 1167069, upload-time = "2026-08-06T14:06:09.49Z" }, - { url = "https://files.pythonhosted.org/packages/91/c3/f1fea5a1f115d04200b3b96c6865fae242b82d333589439262e7a561d4c3/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d1502bd29397daabf22e8d1afd97c794a4cbee2b8c6ae80a1e9912fa5421c9e", size = 1225358, upload-time = "2026-08-06T14:06:11.038Z" }, - { url = "https://files.pythonhosted.org/packages/79/ed/465365db4f2668a128bc77b6a5178eadfe29f572484f7817c2136c90996b/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d4a470e3ccdf5713960208379b248d18a6905e038929d430300396cfd0e937df", size = 1226814, upload-time = "2026-08-06T14:06:12.603Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c2/937f754ea0b1627265946d11161484331f9526d64552c6493d0bab7bcda6/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e0cb17de9c94a60e1b1563e2a0ce02c4b4843ce560d54efed6921dad4decbaa", size = 1424308, upload-time = "2026-08-06T14:06:14.174Z" }, - { url = "https://files.pythonhosted.org/packages/2f/8f/3c03defc0a4154cef71f20aa3ca013118cf6a262141326b3ef4fa7860864/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d78cc7241483fbc291bab543541cc898084607f93ace74e9a61e553915be2398", size = 1244901, upload-time = "2026-08-06T14:06:15.693Z" }, - { url = "https://files.pythonhosted.org/packages/70/7d/0b51e6747bc82ae2a0269d6ed0f9c8bb79e82e68f5b4bea28ffd3e68c43a/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c61e55e70e4cadffafb2a0c0aa518395767145f926887de6464a8ecc7da7b52", size = 1248974, upload-time = "2026-08-06T14:06:17.135Z" }, - { url = "https://files.pythonhosted.org/packages/a3/03/8a31c5f41bde615e3d4e047a07fbb3f1f82461d0edaec1febfc2cb35bee9/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:13718805bce4e7423ce784c03a5595bb3e21b281d7206ee537d5660aea81d3b7", size = 1243730, upload-time = "2026-08-06T14:06:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/01/17/962c29a2b6a59a27dd7350e16488cd72ad70c86114535981ac8c8dbfd579/ast_serialize-0.7.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:85adfb9103e1f7b6774a2342b93109e5748b7040edb888bd417b90f3037ebc31", size = 1293822, upload-time = "2026-08-06T14:06:20.143Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e9/886fe4d14de6c39e4c72e1e34d515ecc363dc51e93e3fc532f2e4fa14bbf/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8bad21d08510972269c8e3d1e331b4f304838fc5af715b8e4b355976e9f73717", size = 1401317, upload-time = "2026-08-06T14:06:21.795Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ab/8fc1de42b1bb4921da96a2fc23409d1a0e692fbdcb6ed9ed5a21a3b5a6a7/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:10993632ef5be1f96bdc890a2d1c1ef00aa4d1cab113d0302cde77862de35371", size = 1502261, upload-time = "2026-08-06T14:06:23.458Z" }, - { url = "https://files.pythonhosted.org/packages/d7/02/c158e0d25b2669492fa138dffbf799a6f8c189edfc057ca104c1a8549422/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c70991a4432d3a5e9efd30f9a3c824649d3631b334c4e1494a2a772cb62110fb", size = 1495376, upload-time = "2026-08-06T14:06:25.065Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fa/874086375aab22aed47ca07dbf351fb8d43ece6a87fc01e048267aba442c/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d8a702bb32455f9b8b9b845db33367790c8584d10677a422f30eb60eaf1ea7b", size = 1556461, upload-time = "2026-08-06T14:06:26.643Z" }, - { url = "https://files.pythonhosted.org/packages/59/48/6d79d19752a74cc263ee09792eb705b2dfdcab550c4b5118cfc280a68dad/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0d36fcd621bd4250fb10601dc70a29607442fc9bbfb345a0856df7c3c6efac9b", size = 1417646, upload-time = "2026-08-06T14:06:28.252Z" }, - { url = "https://files.pythonhosted.org/packages/e8/aa/7d9577e52d62506017522d8699b4ee7105b307b4d56c5530d7b4d2982e93/ast_serialize-0.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65f0f8299e5c02eaba2151ae0ccb7ba5d0061ad46563d1ddd480f5c0dd4a349e", size = 1445031, upload-time = "2026-08-06T14:06:30.173Z" }, - { url = "https://files.pythonhosted.org/packages/ba/a9/b2342d382af078602488d90ff5d26013c8c9fb17826a6550f35cf34ee513/ast_serialize-0.7.0-cp314-cp314t-win32.whl", hash = "sha256:aafda49147e44f9d7a0ca12442fc8baef6926d09285aa920cf06355eed29c697", size = 1063728, upload-time = "2026-08-06T14:06:31.972Z" }, - { url = "https://files.pythonhosted.org/packages/98/b4/44b47d65ef69133a2615ab3558b44eb026b5eff08d8d5c548d771dfc7af1/ast_serialize-0.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ce58ca81e1cfa8faf58eca7dd35f6fe305928657288a9e7d9cb34970796d733e", size = 1103744, upload-time = "2026-08-06T14:06:34.017Z" }, - { url = "https://files.pythonhosted.org/packages/84/23/6c05b9f503bc5c4b6164868507bc3fd596e88e5634f34710b83ca108e91c/ast_serialize-0.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e5f6a61515511b82b211a7978ab76c1351cc28d6089aa946b8f9a9b3fa1d4ff0", size = 1076024, upload-time = "2026-08-06T14:06:35.642Z" }, - { url = "https://files.pythonhosted.org/packages/27/d6/4a95e85a3c52f10dba58e15f9c93ef691cf11a076cedf79817afade8d1fb/ast_serialize-0.7.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:c8c5af5650f2527e758f1fbaaad3deb37c91f1a2d01c7430c63100f51fbb2807", size = 1177734, upload-time = "2026-08-06T14:06:37.17Z" }, - { url = "https://files.pythonhosted.org/packages/a4/9a/c2dca32e435b9c1006f030a65bf487f3bd2e74d38c0e29e1c4deb17ff4cc/ast_serialize-0.7.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:d8ba35d33b1fbd962a7afd835928b1741e90654c81344749b691b92e3aba98e5", size = 1169359, upload-time = "2026-08-06T14:06:39.206Z" }, - { url = "https://files.pythonhosted.org/packages/55/1b/30c73b248905b3de20d1ad11263a5eb5c7d8eec195e5b16e1b30f299225f/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f858a3f274f20ae65f1dccc9408de10c46a27ebf76eac5708793815f2f36182a", size = 1225641, upload-time = "2026-08-06T14:06:40.915Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ed/9f8d0c17598769fd07a3f57eda6a9f67eff729044067bff0e24ec32643f1/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db05954e883cc91310493cde193c1c1acbd00ee8ea583f1eff9be9740af50ceb", size = 1227063, upload-time = "2026-08-06T14:06:42.371Z" }, - { url = "https://files.pythonhosted.org/packages/73/28/42309bc14ca149f57320f6808c175689c656f1779ddaa64313dbf079fa38/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:507f5633ffec9d7cedfd8b90b986c329ceafb9deeaf4cd2bba9782affd83111e", size = 1425301, upload-time = "2026-08-06T14:06:43.891Z" }, - { url = "https://files.pythonhosted.org/packages/4f/75/493961f5deb0b02e688a83b8b9761aed76c8d655519f0cae8388d3ce4082/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1356c62fd91e73786e5f9b9de71f094c8d850d194f652de8c45ee12690534ee", size = 1245906, upload-time = "2026-08-06T14:06:45.412Z" }, - { url = "https://files.pythonhosted.org/packages/56/9b/98350c0b7530218cf0db629ebc39c23f3c8b45f82d29be415cc5a455bab1/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eadaedebf5e3bf0d6dfa309c53283737c668a9e04ea39648e3607ee74cfef904", size = 1250047, upload-time = "2026-08-06T14:06:47.027Z" }, - { url = "https://files.pythonhosted.org/packages/d8/3f/c8f2864f3d088bf0e41af2bb52462348a1d6c24c4fd0b4988b17c6594d78/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:1af0a4509871fd55f15335a5e8f643114c5c5cb485b90fcfa0d8b33861cdcc63", size = 1243423, upload-time = "2026-08-06T14:06:48.571Z" }, - { url = "https://files.pythonhosted.org/packages/71/02/414c98fed866c0b584ef2d294a65561d5eaf5c58e8f23d099ea00f42e529/ast_serialize-0.7.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8530f722344889785da4006f663735e470bfb22fd30eea20e89840ba3cb42fa1", size = 1294528, upload-time = "2026-08-06T14:06:50.157Z" }, - { url = "https://files.pythonhosted.org/packages/8f/4f/b6b207f6fcf03b75c01605a16966e0133c3eb40e07c8e3dc66354ee7d550/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:80a1f859f8f5707848fd92c36daf01d522dbc3f2c066a2d9287e3bec0d79b4ac", size = 1401849, upload-time = "2026-08-06T14:06:52.016Z" }, - { url = "https://files.pythonhosted.org/packages/31/86/676e2b65a919f39b6a04dae6ae863334561e0fbf6d30a403475f94203f35/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:8d5ceed027a42507a65f65746c9b1ada27dd898487306b2ae56b67af54fe5f28", size = 1502708, upload-time = "2026-08-06T14:06:53.707Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f4/d80bef4b92cdff36a58fc21eea99738af7087f8edc26f87c8e9102446d7e/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:ee63de1439b46de948996d81170d7c0b467d8c7068be44968c176539d05f7c36", size = 1496545, upload-time = "2026-08-06T14:06:55.141Z" }, - { url = "https://files.pythonhosted.org/packages/e1/1e/1c7854a500b67d709f9185b3a5c14af6093e8c7b20d1b2d5c8a1edc1ea1a/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:90ecb3b1cebca24299eb84069ad5c0fb0e85d3f062167525dcc89a894641d619", size = 1558827, upload-time = "2026-08-06T14:06:56.884Z" }, - { url = "https://files.pythonhosted.org/packages/c8/34/e806b3768ec4249e36b000021cb31b441438e818141b654bfc1a7efc39e7/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:47cb2d836aa8905f3b14d47cb298f630968544829b93c7eb8a08337f9860e049", size = 1417164, upload-time = "2026-08-06T14:06:58.272Z" }, - { url = "https://files.pythonhosted.org/packages/d4/84/9dc9d0fd28324ee89212ec074973bb4db5b2ff826afb0a045f2aee813a36/ast_serialize-0.7.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:7c54e159fbb62af577b4d20ac2ffb1f56ecfbe8c864a5222a4853adfdb245e25", size = 1446211, upload-time = "2026-08-06T14:06:59.695Z" }, - { url = "https://files.pythonhosted.org/packages/83/1a/02fcdac28c67ae7186a906b8b72ff8c94b7108b4d903a1e0a98260c3a295/ast_serialize-0.7.0-cp315-abi3.abi3t-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1dc8030604a7b1abe0ba3f31572e61312d5bfbafa0ab8cc255f8c285002a9d88", size = 862416, upload-time = "2026-08-06T14:07:01.247Z" }, - { url = "https://files.pythonhosted.org/packages/ea/72/840b2c14b693f40a69ba43c650a88e0dadea875faa1430c8b97dc0613d1a/ast_serialize-0.7.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:cef4d7fa14f6f259acf0c6cc4e9cd7a4ad773bdd13ba28c7d4127cb78e48d2c3", size = 1063825, upload-time = "2026-08-06T14:07:02.694Z" }, - { url = "https://files.pythonhosted.org/packages/ed/cd/6de6248744d30875b875dd8e43a76427b0c2bf42b735ee26149002b9ee04/ast_serialize-0.7.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:537f7a41a7bc1108e60cf8d6f5575beafe5b8a17d1a6ef0e758b4d4e4ad90d18", size = 1105533, upload-time = "2026-08-06T14:07:04.068Z" }, - { url = "https://files.pythonhosted.org/packages/00/30/d17d123c6d6558fad5b7aa026d7df45a639370156cec03d8cf33be9401e7/ast_serialize-0.7.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:913ddfdbbfaa6294dc841579211baa00d789a60d9ae1ad5e04a1e98de11926e4", size = 1076322, upload-time = "2026-08-06T14:07:05.605Z" }, - { url = "https://files.pythonhosted.org/packages/73/73/329d080bb3f1ef96d852fa017617827d6edb38cad31abd0c5f5b7cb5df16/ast_serialize-0.7.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:832e23968712b5b5e052e2095cfe050d32030f2af2d58c6f2c733e8148c82c47", size = 1184035, upload-time = "2026-08-06T14:07:07.273Z" }, - { url = "https://files.pythonhosted.org/packages/9c/48/2bb83025fa197d38f3380357b70adc9f14bc12d87df062dfcbcfeb9e76af/ast_serialize-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7196e1351389df3d1f8e2acffab03db167a14b5bd1f108d8530171b0866f6f56", size = 1177582, upload-time = "2026-08-06T14:07:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7e/f9b13d64699eddc59bae3d027971e7b913750231249319898a4552228190/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a2343625ba33710f7e36a5be36e866ac7dbdd4369f11b5dfc6cd97d4aa85ecf", size = 1234638, upload-time = "2026-08-06T14:07:10.867Z" }, - { url = "https://files.pythonhosted.org/packages/58/e9/7fdbf053f3e35cb2a48d62f57c6a166e475ac9e7421c5004e0b602a7492b/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4251412007121afee236b654b80aca3a8072a073abe2d984c582fb507bf1aa4c", size = 1235796, upload-time = "2026-08-06T14:07:12.548Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/a6b29764a60036fe35dec206b08c4c69a184aeb76d5b0ba00d5ab5acbf6e/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76ca687ac87e97f8621d0be6f470aaa6307e4027ff1c449d36d1fb4f9fee1192", size = 1433051, upload-time = "2026-08-06T14:07:14.13Z" }, - { url = "https://files.pythonhosted.org/packages/73/c9/0eee1122c539407c2a7fbdd2461f33d3863fa20d38614f69f0cd7a6eca28/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:03e4fb60db9185c730db3521d73c6824dfd88d26ab2b2600a9ff1a466a79f0a6", size = 1255585, upload-time = "2026-08-06T14:07:15.682Z" }, - { url = "https://files.pythonhosted.org/packages/c3/d2/33bcfd2507f247b15efd827818ff462b376dd3c637e1576e6f0211a6c0d3/ast_serialize-0.7.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f058bd0b1e44276375730cfb11cd6beba00698dee2bb3d20b831ae319ca3d2b", size = 1258578, upload-time = "2026-08-06T14:07:17.135Z" }, - { url = "https://files.pythonhosted.org/packages/65/1f/61bfa3e75b50f5dd49cac0b8beb1e24d795d0648821bb531e97dff5c1a01/ast_serialize-0.7.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:54c8f5d009b2829bd98d7ebe0c29fcfc8f47f38e85d583d460837c54191b486c", size = 1253582, upload-time = "2026-08-06T14:07:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b9/2db828b7e1830411703e72626b692db540c26923c475f319d637657bb993/ast_serialize-0.7.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7f57b0a8ed664e163294e57b41c52c38527e5a0c73c01ead51f7073e1e652dd2", size = 1301023, upload-time = "2026-08-06T14:07:20.357Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f0/f93e40143f59ee37aebe9e5f8667f3c878232322febc03bb3b977afbf3d3/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a0f4ce65748e2ef28046324254e9b1ad088fae56a93d69d07fbddf97d875b85", size = 1410178, upload-time = "2026-08-06T14:07:21.932Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/a316dddccecedafea3002a9ed1cd6666ab5e64c3094951625b5a1ef95a1c/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:95219c374d0ac76639b26b66cca26e8cb090bf8c515d6bc376af484514d10151", size = 1509449, upload-time = "2026-08-06T14:07:23.53Z" }, - { url = "https://files.pythonhosted.org/packages/81/34/c58f49cc9da933af6ee0f802eb780c48648ae9d0ee9188cb802c10dce29f/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1e71d8879b8c3480eb461128bbf0d5ff87f5ce368fcdb10c850a95010aa5ece4", size = 1505368, upload-time = "2026-08-06T14:07:25.101Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ce/4c5a9ff4e2851ec38ecb8aeef8cb7e0a02308616279c04429342e12985f4/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:85fa81ed590407c6ef6f85cac8e451051bf2623ecd875c4e124af8b8251de3ac", size = 1563486, upload-time = "2026-08-06T14:07:26.937Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/f239d17f902a2ce39da8e6dc034f48f45da93dfda658fa88189a02763510/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:fd018e40c9462235a1d2d101b420cff6c577e5a51b2954ffc05b907a0807defc", size = 1427995, upload-time = "2026-08-06T14:07:28.773Z" }, - { url = "https://files.pythonhosted.org/packages/62/6c/024f58d8b52ce29717c54a578f0bbd844439fcb8eb442596e756fd6eaffe/ast_serialize-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:90a614b8c844620473b7445d31a2a6bf9500fdc4a8d1e2b6a70d40f38beea0e5", size = 1454215, upload-time = "2026-08-06T14:07:30.526Z" }, - { url = "https://files.pythonhosted.org/packages/12/48/f046778df64acef37a36a7338a9bdaa795f46b19d35a4d28bddcd8dfaec7/ast_serialize-0.7.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e6cfb423d4a14d774b3b82f6509adbc5da065246b6ec679221ad133820e0f7f4", size = 868142, upload-time = "2026-08-06T14:07:32.139Z" }, - { url = "https://files.pythonhosted.org/packages/aa/fc/c862cc8d4d8d749f360035baa245fb3098b8b934c57122c0222ac5078762/ast_serialize-0.7.0-cp39-abi3-win32.whl", hash = "sha256:e818854e8521846f5a5271b1488c490231b8b6e02d0158ec42e57bc06dd764f0", size = 1068874, upload-time = "2026-08-06T14:07:33.895Z" }, - { url = "https://files.pythonhosted.org/packages/92/33/e846301850c18fa31598e342bb80b32f380c1bce285df571f5c3711960ff/ast_serialize-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:942921b81440d3ea57f90370d5d29bf28dc81c0778e26b736013e83c7b258023", size = 1111845, upload-time = "2026-08-06T14:07:35.308Z" }, - { url = "https://files.pythonhosted.org/packages/6d/8c/4ee99be6306e72fac6051461794e5cd7895bbbdafe5b921a9a8c4b6fde1c/ast_serialize-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:43b73cf3924aa49f241be6e5f6a19943c8e2e07a2786fc719cacaa54ac6fd2d5", size = 1083656, upload-time = "2026-08-06T14:07:36.828Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" }, + { url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" }, + { url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" }, + { url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" }, + { url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" }, + { url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" }, + { url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" }, + { url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" }, + { url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" }, + { url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" }, + { url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" }, + { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" }, + { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" }, + { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" }, + { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" }, + { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" }, + { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" }, + { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" }, + { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" }, + { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" }, + { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" }, + { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" }, + { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, ] [[package]] @@ -836,19 +836,19 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.4.6" +version = "3.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastmcp-slim", extra = ["client", "server"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/e2c78e26233cd8a416b21513e1925435d54c008a0ec467dbdaa80369daf7/fastmcp-3.4.6.tar.gz", hash = "sha256:2287938da8364ad7071bec2d2393af6ae10fd4e836f06f506569f1456cc87eb4", size = 28808130, upload-time = "2026-08-05T14:54:42.177Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/dd/fd444d94ae7afdaf5b6dd168799d34023f576b405872d6a27d5686a9d1f4/fastmcp-3.4.7.tar.gz", hash = "sha256:43117aca886f5ee2f6a569bba91cef02b59c339aad04ba29950ff18d251c822a", size = 28808982, upload-time = "2026-08-10T21:17:55.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/a5/c02275db111892388972edbb05fbcbfdf1e83cbd1fd03356b3a49b93f839/fastmcp-3.4.6-py3-none-any.whl", hash = "sha256:2a29967be9f68cdd1b4cefb413ede74f83adefd923919a37aaac3611eccdd749", size = 8017, upload-time = "2026-08-05T14:54:38.473Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/6d950459cc831fa17fe2d1797926b6eb2d2f2af50f830e62d0c098cc1ec8/fastmcp-3.4.7-py3-none-any.whl", hash = "sha256:e4e7698cb4af5bc667b1901685261fa2f3526dc73d243a461fca42500c8dbe56", size = 8016, upload-time = "2026-08-10T21:17:51.391Z" }, ] [[package]] name = "fastmcp-slim" -version = "3.4.6" +version = "3.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, @@ -858,9 +858,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/b6/b5b9e81e67a3f39534881d2af6aa3fbd2dc367eaa070c3c932770a0c062f/fastmcp_slim-3.4.6.tar.gz", hash = "sha256:6a1e6e42c697ba90abcb1be617a26947d07316f13c6fa138ae7b0de24558e32c", size = 594167, upload-time = "2026-08-05T14:54:15.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/12/ac/7924e803368d0758ee4d6b1259066550df78f58f0f9f8bfebd5a123e957d/fastmcp_slim-3.4.7.tar.gz", hash = "sha256:06b32a358320a7dc2b2ee040ba89ea55ddc20763dff2949f384f7974b13b5d8f", size = 594357, upload-time = "2026-08-10T21:17:28.723Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/0b/02c254c46b4323ae5262b76a29af73b50a863efc5739a3454d144d3605ee/fastmcp_slim-3.4.6-py3-none-any.whl", hash = "sha256:3e08e6acb03523a47aa17f4d2ab9943e648a41e20b24084da491a732c46dffdc", size = 769174, upload-time = "2026-08-05T14:54:14.663Z" }, + { url = "https://files.pythonhosted.org/packages/b4/97/e0e53642cd029a9a7635ae9c548f9f2cc995af5914e487b3df795664e4be/fastmcp_slim-3.4.7-py3-none-any.whl", hash = "sha256:6c931a0089705f3f2935428ef9b2bc74ad94140adc64aab84d116d103e694b3a", size = 769370, upload-time = "2026-08-10T21:17:27.227Z" }, ] [package.optional-dependencies] @@ -1193,116 +1193,132 @@ wheels = [ [[package]] name = "librt" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/ee/999b97f4b8eb28c4416f9f8a6708077daa1639145cde1c25ac76517df966/librt-0.14.0.tar.gz", hash = "sha256:474eedc5e910d88c1a12193a238f69b2522561d6f10bda9fbe9e70961d2e64e3", size = 214292, upload-time = "2026-08-06T14:52:21.049Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/fe/c84ae5dec0b6fd9a6f3ea5c9aca7a9a96f1e2b7ed9a713eee41b692e2ec7/librt-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dd8270544defd2a25e57f4bf2ad94ff62a74addf7e82a479eea4ce9c5687516", size = 148643, upload-time = "2026-08-06T14:49:19.233Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6d/5248bfe9260b699495f414f7bdf7e52fc7dc7c9a6124bea563182f9d3a32/librt-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36044fb53cc1aa274a69406b67c7c8d90511fdf01389a6cf6d04f7b9bc57f067", size = 153536, upload-time = "2026-08-06T14:49:20.804Z" }, - { url = "https://files.pythonhosted.org/packages/16/b5/be43f0e5cb1ac9601ed7c8416b20822d28784653115369233edc1f268f7e/librt-0.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a2571b37e8eaa23c2aa886698ab5bff51f3d8770a9e83dad44c948ee102aa4f", size = 494314, upload-time = "2026-08-06T14:49:22.259Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9d/69384d4b45273e84089b7fd1c5d96bb66d5ecb0ac41e0070b04e22034821/librt-0.14.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:3b63b5469755d61c634e00223b95496e3098efcf8c7cb68c8cefd4e5294cb289", size = 485394, upload-time = "2026-08-06T14:49:23.855Z" }, - { url = "https://files.pythonhosted.org/packages/e0/b1/2dfc0cdff4e07c473e69997e8db397851470f7a7ab32f30beff4d25d0238/librt-0.14.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1b29b29f92f8ea8b5e8aa427a553786a368726c1f266a5f3116a0cfdb300acd", size = 515383, upload-time = "2026-08-06T14:49:25.264Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9c/cac6921dc1f5ec90e8eeb77a9b49b661d61e0a1c155b96847341fdae88ab/librt-0.14.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2767207d65ca0cf795d60fa67d3fd027ad6612a65c7c9a8286d9cd5a9bba0d41", size = 509450, upload-time = "2026-08-06T14:49:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9b/063e66ea495b894107a7c254ba878e8217ec3312745a795a561d9c9ae693/librt-0.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5ccbd6c9b6a25e5fc2c741691bc77070635ef337b437b8d41bbf1ba917c848a2", size = 532490, upload-time = "2026-08-06T14:49:28.167Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/327b48fd29598f6bc978b37051a013079d1bf222392975a73be542d62d5b/librt-0.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:42fc770d647dbc26f3424715fed2a298e5adcbdce2390f463f0494c0114c6fca", size = 537012, upload-time = "2026-08-06T14:49:29.553Z" }, - { url = "https://files.pythonhosted.org/packages/65/f3/6da0649987c00de52e0389d84bfbf2f87fad367f4ff9ea77b47915fb12e3/librt-0.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7c994b599682ec83e711aaadc0e00b12adc38eaf2d7fee814ef81572332d6676", size = 517105, upload-time = "2026-08-06T14:49:31.447Z" }, - { url = "https://files.pythonhosted.org/packages/89/77/27fa7c9752d0fac645226c2a6c9453f0f755ab5bb4127c6f6277b148c3ea/librt-0.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89179ae255fd9404ac2a423aa58458498518240dfe96dd5e779b4876a83c52bf", size = 558629, upload-time = "2026-08-06T14:49:32.912Z" }, - { url = "https://files.pythonhosted.org/packages/fd/64/9a22a05f8e5292c73d049e923005db8242c29d8c419af34118769775435a/librt-0.14.0-cp310-cp310-win32.whl", hash = "sha256:100cbca2c49533bf5b2cd449d6d499404ae74ecdcb14b337e5547a1404beb9a7", size = 104396, upload-time = "2026-08-06T14:49:34.275Z" }, - { url = "https://files.pythonhosted.org/packages/21/77/a2536afe1c13f16e272d556921ad073d7dfaf3f1a565e00df95bf634bb60/librt-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:e9d9c86af6ae6647abd2f7161e7b2d26d20d0bedf9208b48dddd5f67f13cfbc9", size = 125007, upload-time = "2026-08-06T14:49:35.626Z" }, - { url = "https://files.pythonhosted.org/packages/51/9f/69010fbc4bbf0a19860398340ef46bd62a2c2ac74105a3f409e96500bd30/librt-0.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b77992d446fcf9fcefee57103786f45bb88009b45156863f4b47cd21c6e10c9", size = 148045, upload-time = "2026-08-06T14:49:36.878Z" }, - { url = "https://files.pythonhosted.org/packages/c8/35/dec33c00efadb83cb666d6bcf8561694cefc6f471f0542c22c0840f4cc5b/librt-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ccf121eaf0b28f83221c8c754006293ea4a5e4afff0dc0ebdc90d48520c0972", size = 153028, upload-time = "2026-08-06T14:49:38.297Z" }, - { url = "https://files.pythonhosted.org/packages/93/0d/a91d4a6802fb31068738ef37833ed4a15f007a70e8b77f43a637312caa6e/librt-0.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4db93f3c82a0fca78360a52da98ee1709bfe369b5bbf935dd064f57753c7085", size = 493046, upload-time = "2026-08-06T14:49:39.778Z" }, - { url = "https://files.pythonhosted.org/packages/c3/21/b3c71e5bfc4089a71a76230e6e163a770d9df65c1221c66d274b9b14f111/librt-0.14.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:04f1ece0f80171c4cfef795fa7bf4a75ffe79ec69cc715027d690c8c9b6166fc", size = 485498, upload-time = "2026-08-06T14:49:41.368Z" }, - { url = "https://files.pythonhosted.org/packages/8b/b8/7003a2c56d34d0ee104b292361d442db1f0fcd4679c53fc2a6112a532097/librt-0.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61aca1a405f6d97ec93f676146265677fba3f7c19f779f16d496162853b7816e", size = 515912, upload-time = "2026-08-06T14:49:42.834Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e0/8f0f4bebe4affe3c073bd7ca52abfba3af44565a435f3ef01406c4624495/librt-0.14.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69b592abaa76483401de68c6f52e0ca37687807cbc9928aa126bfd8c11e61b68", size = 508569, upload-time = "2026-08-06T14:49:44.369Z" }, - { url = "https://files.pythonhosted.org/packages/de/01/fd28634391dc1899e0714d67ba935d135025902233c1a6854dc761fd9f15/librt-0.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:595bb8f3c5193cdc7235ad2ab7651b2e823de27a1c4cb83029a57da1e11ce326", size = 530361, upload-time = "2026-08-06T14:49:46.154Z" }, - { url = "https://files.pythonhosted.org/packages/7a/33/b8aecde080731781016180fed0b830cbb40ff9a60a30fa28983c6585315b/librt-0.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ffdb67c2a6925dc4f191771ce9b279c496953639730a343ade0631b222551b8f", size = 534232, upload-time = "2026-08-06T14:49:47.486Z" }, - { url = "https://files.pythonhosted.org/packages/c5/7c/3bd4aa098a4dbbe8ef6b3c851a91d281e19b1785593de3ff5b06571e31a4/librt-0.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5c320c201223605656bda3f07c0536e7629f91a79d84167ee1b71257f0b86921", size = 514269, upload-time = "2026-08-06T14:49:49.013Z" }, - { url = "https://files.pythonhosted.org/packages/61/e2/596e551af59cbc6795dab12653e814b7c069f67ab0134214f285aa945cd2/librt-0.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0660dce4717a345319824ae99bc9036ad887ba5a189422a8c6431bf63d2947c4", size = 557582, upload-time = "2026-08-06T14:49:50.387Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ad/abc3f8fe20618babe70a9864ccaa17b76fa5b7570fc06b35a17315328718/librt-0.14.0-cp311-cp311-win32.whl", hash = "sha256:bd79b5c8a3f09abdc77dee7fea06427bb4cb5fe8e7151029ff9799c75886137c", size = 104900, upload-time = "2026-08-06T14:49:52.002Z" }, - { url = "https://files.pythonhosted.org/packages/fc/04/15bf402734bc8237ce8489cd3ab059810d6c11f19ad04f266bbe5c363e52/librt-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:5878e3f8a09e5dfe862a58f4665c3e56912ab74dec6b3cb74d336e8136ab2141", size = 125843, upload-time = "2026-08-06T14:49:53.47Z" }, - { url = "https://files.pythonhosted.org/packages/4c/48/5c8d94429511443140aa02188a746544e0f47983a7fc62e2767a7d6c7b18/librt-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:0d907a964a0ad582f87726cf4a71bf60f710bdfb2550adbe52070bb88bf1dc4f", size = 111831, upload-time = "2026-08-06T14:49:54.732Z" }, - { url = "https://files.pythonhosted.org/packages/78/7d/ca9feff7e486d74ae3efb5bc66ab95d15fe29090bd902c24be660deed7a3/librt-0.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:171dd324dd6b269503e3777b9cc66c1c6f7bbcbf9b9403b96c5901377bde8f4e", size = 150997, upload-time = "2026-08-06T14:49:56.049Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/9452106e9b2b0f5c771c5656af763927c351b60363e6496fff2775280507/librt-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c648d09e7842f42cb8bcae41b6a39d6536448612ee4c516ce58b284300f5a066", size = 155241, upload-time = "2026-08-06T14:49:57.344Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5b/eeb378de1b84761d555e100e5c69facfa6cb6266ef4a11baab55d64ac6b7/librt-0.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9bb36c48a582caf7d604bc3f3554b550431d1aa6b131a618e33e2816261620a", size = 503094, upload-time = "2026-08-06T14:49:58.775Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/e5c372c7c543cbe721b25ac6837190f27d627838a9208b7069e167c06f9c/librt-0.14.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:93c52e7d5f9ac110db854a4c49cc5fb0362e77047e7ebf71f1a6f35829395d47", size = 496536, upload-time = "2026-08-06T14:50:00.43Z" }, - { url = "https://files.pythonhosted.org/packages/e0/81/9d64cc59740a37a68e4bcad1fb1734a354c6e2899cc66871feff4c1a9032/librt-0.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31e9931b28896420c0870332eefc8e966121adf8013850d14a22d694c17c8cdf", size = 531811, upload-time = "2026-08-06T14:50:02.018Z" }, - { url = "https://files.pythonhosted.org/packages/87/93/c643336b08dfd38e27a77ad3811ec7f18c2aa83677332f21300430ad5fcb/librt-0.14.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:52e3630f4d00aa1fd1c224e2ae7ea2a31058493247f765393b7aac2bd0f3bf01", size = 524425, upload-time = "2026-08-06T14:50:03.528Z" }, - { url = "https://files.pythonhosted.org/packages/3f/19/aa78ba06cf8546a0f3d5ef6985b721d4bad60f1d938e147b6576f826dae7/librt-0.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c66f6183f4dde136d5567fbd192c86236c8a89caa73f2720f2ed94c176194041", size = 543060, upload-time = "2026-08-06T14:50:05.15Z" }, - { url = "https://files.pythonhosted.org/packages/f5/18/a21935834a687940ad83ecfc14aa9118493f0da53b6e9861082f8cae2381/librt-0.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62d566b4b4f6510d471fb6fa1b6bf8d183a1039d2c058d85f7481ee24ee3d27d", size = 546840, upload-time = "2026-08-06T14:50:06.571Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d1/98915036feb2315cabd700f25d9915969f81a8391d2cd9c0cc93df6f7116/librt-0.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:069a5790f1ba9b6abd882033a486b5a5b68754854b7ad015ee5ad8da1d23e11f", size = 535732, upload-time = "2026-08-06T14:50:08.478Z" }, - { url = "https://files.pythonhosted.org/packages/40/e4/280b07ef374464ca550523ed82049bffe8e06f73d11ee947543389b3cd23/librt-0.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2ec4541a9e522871fa2f2983478d7ee9cd994af1e4718b7e291904c15430b062", size = 573579, upload-time = "2026-08-06T14:50:09.949Z" }, - { url = "https://files.pythonhosted.org/packages/9b/75/773489183b257cab2f341087d9327155f7763c1fab07984a8d748554bdfa/librt-0.14.0-cp312-cp312-win32.whl", hash = "sha256:1697ebe1612604233cd69383c4369c91a40f7d8ad1bd890e1647acec35700f32", size = 106097, upload-time = "2026-08-06T14:50:11.414Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7c/f60a3379723295761403ae6301be16afa98b389dee8c6aa1b5de35794d76/librt-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:3e30f225dc2598638a03bd0fd56d479e09263fd769298af57711c2c921498e59", size = 126933, upload-time = "2026-08-06T14:50:12.743Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d5/08147d10a6e5ca676dc0e140b10758a83de73fc863712f167751f2aa8bca/librt-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:d6c98dd32e0f8d1a701bcfd470de0d09972d725c8dbad3cf9791b5262caae1a4", size = 112237, upload-time = "2026-08-06T14:50:14.12Z" }, - { url = "https://files.pythonhosted.org/packages/a5/1e/eb17e048ef320dc3c780f883c25e8bbcb32d9b99baee4df47743dd19fd83/librt-0.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e483dcacff2ebef0e390002475fafe026f1171632e88e1a8bc69bbc4fe31d92", size = 151028, upload-time = "2026-08-06T14:50:15.505Z" }, - { url = "https://files.pythonhosted.org/packages/48/0d/a08a4e73990d401031d17f2ac25de61e32efbfc2f195903a5f41782ea2ec/librt-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7a469d3a638cb1310f177ad6263478661c31929628e04ad8c64eae43117aa935", size = 155140, upload-time = "2026-08-06T14:50:16.866Z" }, - { url = "https://files.pythonhosted.org/packages/73/97/5fbe0c8f05a549678180c56f0792aaa85462c4184645a0dadb720e1e7edc/librt-0.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a97882a260cc133838c4eb5083c1d39c6dd9c5bb0ef88f052f7ee078cf132a", size = 502531, upload-time = "2026-08-06T14:50:18.292Z" }, - { url = "https://files.pythonhosted.org/packages/0c/19/b1124bbc3b53884726b36feb5893f17c64638560f363e474af1ca808a961/librt-0.14.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b8bdb74dc214de53fd337adc08d01decf478c94b98eb71de8175476499b5f094", size = 496114, upload-time = "2026-08-06T14:50:19.722Z" }, - { url = "https://files.pythonhosted.org/packages/04/1e/ce212234460b1420d223c6d531579e6192d2529624cd4037fbb46da0041d/librt-0.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b75736fc71bf792451f3c09f6924e3f1a456d60bd9e522205229f6a4501b1dac", size = 531575, upload-time = "2026-08-06T14:50:21.313Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9e/13a82455687f65d52f886b4189d1f1546d55c76441984b490f4fea0beb44/librt-0.14.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc4f4191d97ca3e5d1c9dea91b9c363e727ca01a4ecd8cb75c150d262e0be6e6", size = 524444, upload-time = "2026-08-06T14:50:23.043Z" }, - { url = "https://files.pythonhosted.org/packages/4c/29/30b4b024010410bbd2145e8f1d09568465bf0e9a8ae74b689bb605cb1567/librt-0.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:079407feefd3746de68a73918e9ef43d0c5b791a3b198a9488568fe42baf57f8", size = 543090, upload-time = "2026-08-06T14:50:24.749Z" }, - { url = "https://files.pythonhosted.org/packages/ad/08/befe0e170b1063ac49d4819c2d4854698656a9dd404c4c9d62e00b426bf2/librt-0.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67d756804347354df8bb9bf1e9f8b7565b4d0528cf14406d543937bca04d8545", size = 546405, upload-time = "2026-08-06T14:50:26.192Z" }, - { url = "https://files.pythonhosted.org/packages/e2/5d/e55773f575c8ea1d33b29c2b6883f05ee109a112c764f7127a09e0c288c9/librt-0.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f91abb5c73148dc49188ae47341902432893ebca5cf12c60f03b25615ac906a2", size = 535995, upload-time = "2026-08-06T14:50:27.829Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c2/83730da76d7e273163da50184fe04cee3678fe65f7c8002fe33811c5a621/librt-0.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7574c4ee6d9df5a3de33d54b0e93c4a069a4671e9a757c7ca8fd93a8635d16b", size = 573590, upload-time = "2026-08-06T14:50:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/3e/bf/22e882115f94276a7d922c63af87ffbd0cdf5e3f545d3dd75dd8e7a9614d/librt-0.14.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:184b24430f480b4e4f910cd5dbcd22eb1ac2ef91aa98822a774acc032ed17ec3", size = 82189, upload-time = "2026-08-06T14:50:30.571Z" }, - { url = "https://files.pythonhosted.org/packages/62/70/0389b1e1a9ee1ed73751cb18decf3a33bdd19781c9fadb46415271720081/librt-0.14.0-cp313-cp313-win32.whl", hash = "sha256:48112eff5a4fecd8919260363f35009354909a48bc49c100ca2a93b6b344ea46", size = 106198, upload-time = "2026-08-06T14:50:31.794Z" }, - { url = "https://files.pythonhosted.org/packages/23/19/c38ecd86f649a1014bc0bfcd9c6ce620491f67a42975929dbeb87409e9a5/librt-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:6ed36b53455622ea42b20fa776f6ab7fd15225b417cda80d5def848f66a692c7", size = 126963, upload-time = "2026-08-06T14:50:33.076Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2c/9b485d945e64e94cdc703aab7a2b2e88dba27c8b1bb1667ffb301476812f/librt-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:9b24351679fce00a3609505d6898a098f554ea00dcbd2babe89fd8710bb27809", size = 112127, upload-time = "2026-08-06T14:50:34.331Z" }, - { url = "https://files.pythonhosted.org/packages/0e/99/47fcbdbdb5031a90533573293fad645aada913e4da402123586622c313b3/librt-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:514e125185753c938686bc2262a4a02598430e32409d9a986af518826c870d0a", size = 149815, upload-time = "2026-08-06T14:50:35.693Z" }, - { url = "https://files.pythonhosted.org/packages/63/c2/4ce5142e0056c5b2334cff0173b8ad4c79f67b75b612eb9a4139a560cdfd/librt-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b15f24c285a6718f284da5cbf9f199222618bc3830715f3e6f7188fd76b27dd9", size = 154047, upload-time = "2026-08-06T14:50:37.226Z" }, - { url = "https://files.pythonhosted.org/packages/18/e4/c589881658f624873f7b11d4e834c3b84e103a3cef83b8fd8074907f0d30/librt-0.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:522ef1b2de9293d43edf5a5aea38a7ce32c747c6411e7b5629e23bc5346d7d3d", size = 494157, upload-time = "2026-08-06T14:50:39.615Z" }, - { url = "https://files.pythonhosted.org/packages/10/a5/e100af06bc6310e1a1d714ff923425f2ec40c7f77861b4ef3aff708b5a49/librt-0.14.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:079b69b63269ed955fd0c087ac5c24201b522fa1b1ae915b4a489e4ff156ca01", size = 491062, upload-time = "2026-08-06T14:50:41.265Z" }, - { url = "https://files.pythonhosted.org/packages/35/e8/bf985c4e5cc6826f6c6095e2b097b96552665b18d88744816f10f42ad0de/librt-0.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2fd8e75154eb85cf18927fe034c150a6c3fe1c385d1c79d0e5f32ff7ee1f707", size = 522987, upload-time = "2026-08-06T14:50:42.722Z" }, - { url = "https://files.pythonhosted.org/packages/c3/46/f087b1560f840ce0f740f83a8deef2f96e7d21449ade68404a4296f775ed/librt-0.14.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f0a50d043481860050ee965c5c8c89b9f4024023868b9ba152d32a2117bf077", size = 515075, upload-time = "2026-08-06T14:50:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/be/a1/049d31b66a80018e50e315a562d008b8ff0bb3ef84308c47d8e289bcb80d/librt-0.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c1cdf9eeffa6925fc903003cfc9575cf779734c158209df4fabdcfdb4391425", size = 534028, upload-time = "2026-08-06T14:50:45.712Z" }, - { url = "https://files.pythonhosted.org/packages/cd/10/d1b646768500cf969a5ba1b180fa460283043066a559c10d24b9354d2dd6/librt-0.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:371764994ae96aad32cfe3b3d469b430ddd9cf07f7e15ab0cd225d7a4c1a94a3", size = 540547, upload-time = "2026-08-06T14:50:47.355Z" }, - { url = "https://files.pythonhosted.org/packages/5a/91/5992827caf0fc8805592676d47edd013435abd365f76c5a483366afee7db/librt-0.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d6deb7d14a6b0b95c6e9137c6f8071805a9bd07f9a1f50b7a521626c14379ea", size = 523228, upload-time = "2026-08-06T14:50:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/41/2a/337c64452908b11de1ced2b25611d5cffd73e2ada2e9868184cd1a332765/librt-0.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:097cf99296b1a4588dd5108df7c115dee76b07c7484975d36fa4263277ab9d40", size = 565751, upload-time = "2026-08-06T14:50:51.038Z" }, - { url = "https://files.pythonhosted.org/packages/5c/28/ea6fe7fe24eeff816a2205994ca5b0e77b405c556cba06080dadafacf8a0/librt-0.14.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a80394c9af4b641a2daed3b45948c05c7022997b8e7fa3e1f104e896c423178e", size = 81612, upload-time = "2026-08-06T14:50:52.617Z" }, - { url = "https://files.pythonhosted.org/packages/c3/31/d9d2fa174db79ccb40e7333ecd5a68ac6eac650baccd405095e3bec77ac7/librt-0.14.0-cp314-cp314-win32.whl", hash = "sha256:254b4e5a894c496f753d42f9d24f1eaffbbbbf39d744849464ab3db0a9408d1e", size = 100111, upload-time = "2026-08-06T14:50:53.91Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d1/6c6656d9e66b81ff4b6bb6f7e46d16a750fcbcfbdb57db9d8336b8b23ace/librt-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:af5360691ed0f4573bbf1d19aa5f375866c3d7d6259974dab4e156e42c035788", size = 121216, upload-time = "2026-08-06T14:50:55.271Z" }, - { url = "https://files.pythonhosted.org/packages/19/3b/a1abbed7e4cf2969ab71140d3238c9c7a51de73af5251547d3a3be7ca8a1/librt-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4e566a22f852783e6a37a834e6ab5140074581dfc446c76b92e6c87ed81ef459", size = 106395, upload-time = "2026-08-06T14:50:56.603Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9b/2467e569fea8b180001c799c5956e295ca332989e0133dce0c0d03a0bafe/librt-0.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ca165b42e1d5f0fae2f15f6e3ecc27a7bc37f6e81263147233622618335d5453", size = 159534, upload-time = "2026-08-06T14:50:57.879Z" }, - { url = "https://files.pythonhosted.org/packages/06/60/2eba56188f754793a9b1ccdd6cfe2b3b9ebc2d6e786a6801715bb37f5d73/librt-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b023e7201d954a89707ab7df2850f7d022ce4c70fc94dc06582245a3704d6ea", size = 161605, upload-time = "2026-08-06T14:50:59.179Z" }, - { url = "https://files.pythonhosted.org/packages/50/86/4f172345626c740e7a1f467de357be3a1f1c246e6dcae7e44bac91fe581f/librt-0.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b24337d08efcf1b541244d02c396818d5bb35d38399eca13e09b5c5c200ee5f", size = 701752, upload-time = "2026-08-06T14:51:00.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/34/a4db6345633b15f6a154e7933d0df6812cc25a3f98dc7b12f881cce56b47/librt-0.14.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f744cc71a99cd8ba017be77d0d729f76d616c5047fcc9f77a6c1f93fdc2b1c89", size = 682080, upload-time = "2026-08-06T14:51:02.283Z" }, - { url = "https://files.pythonhosted.org/packages/79/cf/93c04412ee704bcf6db6580aa3ed82d05b319cd6ea65cacb64b1874a4005/librt-0.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c469c5f0d5b577ea60e6a8cfe7932cd610e80370d18ecea6df8d2a53b07fa39", size = 722483, upload-time = "2026-08-06T14:51:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/87/e1/1c0ad3901554c9637e8d7cb1afa4c1227323fd5d9307aa4e9d2cf24f032f/librt-0.14.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1c3b5327c425490b45a8cbf81cc3d858d0bece361f712e86fc9940795bf1a05", size = 729657, upload-time = "2026-08-06T14:51:06.099Z" }, - { url = "https://files.pythonhosted.org/packages/b3/69/dcb83a2baa657aaef0cea242fe78c99bad4c6322313587f7b52c3d2fee05/librt-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2ac2a485f64d3fdce7b07ebe6717e1ccfa9b52542049f2148b8532e4905bd199", size = 752808, upload-time = "2026-08-06T14:51:07.826Z" }, - { url = "https://files.pythonhosted.org/packages/b0/01/f07fe2748f75850254d78fe4fc90120903fec1610ffa84d86d85cfbd2025/librt-0.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b12ae9feb16b86d4e648a7abac72b871c63b8465378a8624b639ad4259f469cb", size = 745241, upload-time = "2026-08-06T14:51:09.399Z" }, - { url = "https://files.pythonhosted.org/packages/1d/32/c7a0a4db94805c05c091c6ef073a038f5cc8f17bfac2a5a58fbffa0a30df/librt-0.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cc6f16ce49a756d66d5ab5ef026001de879ded7a5854682de7bf3e0c3a2d9851", size = 727506, upload-time = "2026-08-06T14:51:10.943Z" }, - { url = "https://files.pythonhosted.org/packages/2e/79/6c8867ccff54b87eb2d5ba93bbe2e0a00e08a18c429b1df839ae3434a84b/librt-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7650be33066c5edce26773dd8e4b124eef05878986f404b0c3dcee47f0812073", size = 774306, upload-time = "2026-08-06T14:51:12.568Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/16ab8a5c7c2782b1e54d26369d1373f2f3a37fa616fe391a5a17bc455c4d/librt-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:388ee73e43c1845e32195bca0cff85d0661d0898ff244d1bbb60d9a2e40e44f6", size = 104357, upload-time = "2026-08-06T14:51:14.094Z" }, - { url = "https://files.pythonhosted.org/packages/fe/f1/260c2593a24cdbf47a6044a78185be51c18b843aca0462c0afa08d5ff5f6/librt-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8dcd1657a9e81cc83bc32bcaee44d71f3ccf492dcbcc26e1d5f2f80bfd474d93", size = 126988, upload-time = "2026-08-06T14:51:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/42/3e/3649576800fa2509a23dcc658aa60698d759382ecede3a27652e03028394/librt-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2bf5a2a67aded654124e7af37d7e5aa7c589d34e18bdd468cb9c8f509c67eabb", size = 110768, upload-time = "2026-08-06T14:51:16.66Z" }, - { url = "https://files.pythonhosted.org/packages/c9/45/b3d3d5c8bd05fe86fd19c9a1c72a0ba797e39a309b329023f3bdf596e8eb/librt-0.14.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7c458143b671357394070b9ec71011825141a74f7a55f4315bccaa61888a11c1", size = 149813, upload-time = "2026-08-06T14:51:19.269Z" }, - { url = "https://files.pythonhosted.org/packages/86/07/d9a4bbc6eb4186f0c2072e6d189c13e9afeb8efd65084cce6d950f9ad887/librt-0.14.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:611fb701a8c9f7d3f71e3bac5eba5f184a52a5816f1e262a003bb99a19a16f79", size = 154495, upload-time = "2026-08-06T14:51:20.81Z" }, - { url = "https://files.pythonhosted.org/packages/83/66/1979a4f04158635917ce284e8dda4b4bab5957c09437ee44f895750eaf9b/librt-0.14.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f088e040c0409ec2f6ce0b5aeb7ab58afbba1f66c3697d6820cb88cac9f3be0", size = 497485, upload-time = "2026-08-06T14:51:22.35Z" }, - { url = "https://files.pythonhosted.org/packages/0c/24/ee15cf54085c75c8ca4dcccc2a2f1f72e0b733bef28e7b579f21222d5da6/librt-0.14.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:1c51d27c64260ccb24e6861b702100cf2e0a26f37c0344fac22072e8907a52a9", size = 480375, upload-time = "2026-08-06T14:51:24.134Z" }, - { url = "https://files.pythonhosted.org/packages/ec/01/ce8fcbcbc17d6ded94e65e542c490e30eff93e0f9e37308af1f3c5d9cff5/librt-0.14.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f796372a81dc39918c21d67479c4e3716a34b0236efeeec1fd555f453fb8c6f8", size = 525019, upload-time = "2026-08-06T14:51:25.894Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/6552109d0d060b8b7bfe36a79179ab7c6d3afb4a48299b23e8613192f9db/librt-0.14.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7bcbc90ce85729f4c9933b7ab33ec98389d40a69be3e71d2e94ac2e9de6c032", size = 520324, upload-time = "2026-08-06T14:51:27.448Z" }, - { url = "https://files.pythonhosted.org/packages/98/c1/1e27979381b1504b8a2023459df3f9c739199eec9bdcb513f1a9f24f6e2e/librt-0.14.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:af53c610bc5f71c3d2a73c6525cc90d5765ce45f1fac67310da6199cb4c2dd65", size = 537131, upload-time = "2026-08-06T14:51:29.055Z" }, - { url = "https://files.pythonhosted.org/packages/97/6e/fd9401758881a69e2f615185c35659578a9cf033081ea28bdde82eea901a/librt-0.14.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:fbe1226468cf0fd6f9ac5f58fc5eb5171221c51061e6a768285c683016b58a45", size = 527339, upload-time = "2026-08-06T14:51:30.663Z" }, - { url = "https://files.pythonhosted.org/packages/68/9d/aeabab4ad00c43e6fd494749fd7567d5b31b01790b09afea4540974b4536/librt-0.14.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:33230fe017a0528fb403127917c04efd340721858bd8ff3f1beaca8a41c302f3", size = 529624, upload-time = "2026-08-06T14:51:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/c5/26/c607acf7e900bb010e9221ad8b42e2b5433ffff5b9837d19cc9ef6c641d4/librt-0.14.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:492eaa4e1d3640f14916a7ce0a3f2f6acf1dea87b0edc2c382c28b94003c6430", size = 567641, upload-time = "2026-08-06T14:51:33.977Z" }, - { url = "https://files.pythonhosted.org/packages/bc/be/54bbd5cf7b6d8bb95ee7552058ed13d7e27630d8a8844fb7a2b913cc6664/librt-0.14.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:4c955fecca2557ff8daf843537f2d9f874525b23f7e811641e90e48660cd8c0a", size = 81666, upload-time = "2026-08-06T14:51:35.471Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1a/5f822cb827a3344d9a518b25d1f241d1d551f122fca3bd65fdb2f79c674d/librt-0.14.0-cp315-cp315-win32.whl", hash = "sha256:9238a60060e6a6400fbbb6d70c1d50974c5951eb0eda7433a4b1469a28cc30d4", size = 100047, upload-time = "2026-08-06T14:51:36.775Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/12b9efe0c391c363e11ae05f2a5ef17c7ae6f3bd53894a787317b35caa7e/librt-0.14.0-cp315-cp315-win_amd64.whl", hash = "sha256:07f34798827de9a3922ed748ae44cd82292ca7a189dbc480326fa5eb7a2ce18e", size = 121195, upload-time = "2026-08-06T14:51:38.224Z" }, - { url = "https://files.pythonhosted.org/packages/f0/39/eb506c95f04436093cc32b44153e9abebe8077554349aa41cf4d5ace4c01/librt-0.14.0-cp315-cp315-win_arm64.whl", hash = "sha256:f8894a73e9003996a1a03b0d340057775381e9e224c27e9cfd8af47fdd9ed565", size = 106414, upload-time = "2026-08-06T14:51:39.725Z" }, - { url = "https://files.pythonhosted.org/packages/c5/4b/5c6f78d3d378f51197e9ce6cba90581e7066629730fccc3b76e32da2b5fb/librt-0.14.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:656ec51037ad010cd3388abba2bb73e5633b244ea5365de2afc767a1b3358353", size = 159419, upload-time = "2026-08-06T14:51:41.132Z" }, - { url = "https://files.pythonhosted.org/packages/06/7c/bbd8a856fd906fe3c1485ef7b09f1f93d558984ac2ab7e04aa0b9be7c7ee/librt-0.14.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:7cb23813b4b88d32481c78adaacbe5cc19318b2989a28fba40fb400c529450e8", size = 161688, upload-time = "2026-08-06T14:51:42.552Z" }, - { url = "https://files.pythonhosted.org/packages/31/ee/c388033b154f8eb0749b77b31d4eee8423a5c6e4c7c08c5617e2715b404e/librt-0.14.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbb8f0536a83bbaed37d0f3cf0b5002d1069562de0d24a72b87f2669f58b08b0", size = 710640, upload-time = "2026-08-06T14:51:44.161Z" }, - { url = "https://files.pythonhosted.org/packages/ef/cb/fc317282a406327037a963264dfe9a15c652b8520168b701ebf947292ccc/librt-0.14.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:58f6fd779181d37cae30f3c15a3a454dc5d1082af00111030f03611c0530afbc", size = 679347, upload-time = "2026-08-06T14:51:45.704Z" }, - { url = "https://files.pythonhosted.org/packages/1a/25/e6c84d91ffa923a772def1358bffe7aead7fb6d375455cbf307af6afc4bf/librt-0.14.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6127d8d81544e1aeb5a8fac3c4b713aa221056c2d1235b36b20acd95bd7c869", size = 729771, upload-time = "2026-08-06T14:51:47.471Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c5/90af116b05e346a8ff7e4463a459e9946a21b1f6bb06e1174a7baaf0a24c/librt-0.14.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22609292248d1b3a7f28d06f9cfd97b447ac9893f3ff935a321d5caf722dbece", size = 742694, upload-time = "2026-08-06T14:51:49.167Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a7/bcbc4ab5469f8209965d1e582a7acb86188cc7fbb5dc15822460ecee80e3/librt-0.14.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:726c499444e440dd7731b0105ad6daaa28c7fb2806f837ad7dca770ac7991d04", size = 763374, upload-time = "2026-08-06T14:51:50.673Z" }, - { url = "https://files.pythonhosted.org/packages/4f/d7/38f24ba16854834efd4d8adaffe0f43fc19ef42871b1bc53c6ec73aa37d7/librt-0.14.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:e5fa45343e5327f9e3c365ea24634171cb66733339d224a892aca3257b7e83f7", size = 743212, upload-time = "2026-08-06T14:51:52.24Z" }, - { url = "https://files.pythonhosted.org/packages/9b/db/37cdb2f96348403ef8a830cabc574f816e0697a987a63a7288e10aa479d6/librt-0.14.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c88e8d3c05b41c7578746db8be5202d2daaf7987bfd6f4d2f7ec60e267671f2e", size = 741904, upload-time = "2026-08-06T14:51:53.871Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d1/3487e262aecb743b651614d747732033e99b9bdf23a76e17adb30f36b42e/librt-0.14.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8de3ffad34619b8f0f8cc02ffebea71883fb53b994903c9150f750a6df9165d7", size = 783665, upload-time = "2026-08-06T14:51:55.551Z" }, - { url = "https://files.pythonhosted.org/packages/d2/de/e123239b4806f3cf9324871478bb5a3da9fdf78a4a1eb4618449b737cf96/librt-0.14.0-cp315-cp315t-win32.whl", hash = "sha256:36d0695186da49f4e9323869e50767391772a04c7bacee4ad3d85dd69e671154", size = 104287, upload-time = "2026-08-06T14:51:56.975Z" }, - { url = "https://files.pythonhosted.org/packages/e0/78/b1b1efbe13d8e31859d077aa2ef02719f3a03392cb50aa09fe7c84770940/librt-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:5982d33852bef29eae68bbc62444b793fbac2fe366acf27eaa8051138ddfabdd", size = 126847, upload-time = "2026-08-06T14:51:58.411Z" }, - { url = "https://files.pythonhosted.org/packages/4a/78/fadc5159a101366998ddcc505e43d24610222c3780ce2c47790676d9f44c/librt-0.14.0-cp315-cp315t-win_arm64.whl", hash = "sha256:14fcc7495ee5a0343b33713139a059e35929a4c337cc276774508a9448a4f320", size = 110636, upload-time = "2026-08-06T14:51:59.759Z" }, +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/12/e2e9ca532cf5a0e08c9489826c4a35c6958c92ba0313fda70e8c6c3912be/librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489", size = 148673, upload-time = "2026-08-07T10:46:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/6d/7c/02005e23478bd5950618d9712e0fd2b4c511657857f3efd8ba6a5feabcdd/librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52", size = 153547, upload-time = "2026-08-07T10:46:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/a0/90/d8848a735f5642077fc4b3b4bebcdb08edf10178e3add45597f5201a368f/librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8", size = 494355, upload-time = "2026-08-07T10:46:25.122Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0b/8604f41ea02feace490e9e405a338a15f9905369f55b239a9ce31c946f24/librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1", size = 485459, upload-time = "2026-08-07T10:46:26.447Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ac/84153bda1ce0da609182527ab92b40d961809e544eefdc5a1c2422971416/librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d", size = 498398, upload-time = "2026-08-07T10:46:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3a/5ca6cd282b2c244bec8ec84102e09773264e9c02891d56ab3a8f0e4d7083/librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702", size = 515474, upload-time = "2026-08-07T10:46:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/73/d3/bd34110234779eb843c6ed66aba7c9b2091d3dd85989f1fb9922f564cb7a/librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9", size = 509484, upload-time = "2026-08-07T10:46:30.124Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/43c3f7f071d71631a7daa3b835ef2168ea39f20692d81464d4e47fbaa6d6/librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b", size = 532534, upload-time = "2026-08-07T10:46:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1c/b854adf036ea817c40408873a5b794d65a91d9f0f39826f2ad2a2d5d7f48/librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db", size = 537087, upload-time = "2026-08-07T10:46:32.734Z" }, + { url = "https://files.pythonhosted.org/packages/25/5c/c9a890e244e7dd725d3bd8b560e41f0aec787eaf343b46956a290ab7b841/librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451", size = 536575, upload-time = "2026-08-07T10:46:33.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c5/c8e70b60b704299555f55db468eb46b1c81bfc60201ffbfe20407d89870c/librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389", size = 517142, upload-time = "2026-08-07T10:46:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/767a90c41f5d381b3195bc88ac0ec4afda35777c9c781e1f9848fedd965e/librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c", size = 558714, upload-time = "2026-08-07T10:46:36.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/3c0624b8dc8301ab808f2b3a910995bcabe28df070fb9a0e5505ae997dae/librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e", size = 104426, upload-time = "2026-08-07T10:46:38.594Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/e91c0382304bedb2db9c6801897319a9dcb68daac5e975819b562362f20d/librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053", size = 125057, upload-time = "2026-08-07T10:46:40.052Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, ] [[package]] @@ -1778,11 +1794,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.11.0" +version = "4.11.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/98/0bf930c4f97d0266b58a89e36c015f56232c52b5d2f207215d48cca9e8f7/platformdirs-4.11.2.tar.gz", hash = "sha256:3a2ae5fca3520a01ab1be8b45613537f52ddf5b5f6f53d88233892dfbf0cd82d", size = 32716, upload-time = "2026-08-10T15:48:06.092Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, + { url = "https://files.pythonhosted.org/packages/49/e2/4e6eee633809c376c024821b91ade709cbfd040ec53939ffbcc292aa7eee/platformdirs-4.11.2-py3-none-any.whl", hash = "sha256:7f89089b6ea71bda7962953edcf784b2e2d9d285b40ad88be2bb75c6e9d82ab4", size = 23361, upload-time = "2026-08-10T15:48:04.855Z" }, ] [[package]] @@ -1967,16 +1983,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.2" +version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, ] [[package]] @@ -2548,27 +2564,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, - { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, - { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, - { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, - { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, - { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, - { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, - { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, - { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, - { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, - { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, - { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, +version = "0.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, + { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, + { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, + { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, ] [[package]] @@ -2595,11 +2611,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.9.1" +version = "2.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, ] [[package]] @@ -3003,15 +3019,15 @@ wheels = [ [[package]] name = "starlette" -version = "1.4.1" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/3c/76d2fd1f1357ed0f0108d8a5aa233dcf16e2946a8559c84912fe08e01ac7/starlette-1.4.1.tar.gz", hash = "sha256:b7332de6e9375593a29ba9eee1e6ecfeb3eb2043e2e19a13b4b71da73ff35540", size = 2709041, upload-time = "2026-08-05T15:17:26.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/0a/67e95f21498de41433babf7b1db0eeab449eb58872dfb831b27747a70fd0/starlette-1.4.1-py3-none-any.whl", hash = "sha256:7d078e0fbefae0d2cecfb80a799d6fb84b1c0c6acd4f14ac79d17d0e7ec27f19", size = 74019, upload-time = "2026-08-05T15:17:24.357Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, ] [[package]] @@ -3100,14 +3116,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/bc/4eae18cd40c65798a16267572ba346c11f599d44b01603dbd843342042bc/typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d", size = 76711, upload-time = "2026-08-10T09:39:18.063Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/42/f7/7a3935abdebd5cf18705a5f0335dd6a3a18bef3baa7cb9edc3b6b9922cc8/typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd", size = 14693, upload-time = "2026-08-10T09:39:16.693Z" }, ] [[package]] @@ -3121,11 +3137,11 @@ wheels = [ [[package]] name = "uncalled-for" -version = "0.3.2" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz", hash = "sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2", size = 50032, upload-time = "2026-05-06T13:38:25.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5a/92ce0b3ea5481915f55da994c2c2c5f7a3c09949afde196ee89f8ab961aa/uncalled_for-0.4.0.tar.gz", hash = "sha256:335b95bd2422332ec210d518f314a16e4c640921c39fc8bf2ad095bd3538f4af", size = 56979, upload-time = "2026-08-10T14:51:46.247Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" }, + { url = "https://files.pythonhosted.org/packages/a2/40/97cec87c077eb3291fc7905e6633e08b7ca593c57d30238444bcb6bb3d53/uncalled_for-0.4.0-py3-none-any.whl", hash = "sha256:16c4bb3337532e4bd5569adc192285976f3ad5305402256d34c67a12b5c968bd", size = 15502, upload-time = "2026-08-10T14:51:45.068Z" }, ] [[package]] From 1e1037d3bf2be59331ce022f382330931a294747 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 15 Aug 2026 06:46:25 -0500 Subject: [PATCH 33/71] .tool-versions(uv) uv 0.12.1 -> 0.12.3 why: 0.12.3 is the newest uv past the 3-day window this setup applies to package resolution (`exclude-newer` in the uv config, `min-release-age` in .npmrc). Toolchain pins are resolved by mise and are not gated by those settings, but holding the resolver binary itself to the same window keeps one policy rather than two. 0.12.4 (2026-08-13) and 0.12.5 (2026-08-14) are both inside the window and are deliberately skipped. what the release changes here: 0.12.2 adds CPython 3.14.7 and 0.12.3 adds 3.13.15, so the bare `3.14` and `3.13` entries in this file now resolve to those patch releases. The rest of the span is performance work that lands on every resolve: uv.lock parsing got faster for wheel and source-distribution entries, Linux startup initializes the workspace cache before spawning a thread, interpreter discovery no longer reads procfs, and conflict-heavy resolutions avoid materializing range complements. 0.12.2 also stops artifact sizes recorded in cached wheels from breaking older uv versions reading the same cache. - uv - https://github.com/astral-sh/uv/blob/0.12.2/CHANGELOG.md - https://github.com/astral-sh/uv/releases/tag/0.12.2 - https://github.com/astral-sh/uv/blob/0.12.3/CHANGELOG.md - https://github.com/astral-sh/uv/releases/tag/0.12.3 --- .tool-versions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tool-versions b/.tool-versions index d03a9772..823ef64f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ just 1.58.0 -uv 0.12.1 +uv 0.12.3 python 3.14 3.13 3.12 3.11 3.10 From eba3e39a349521eb17d8f7a0c94291392ee62f75 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 16 Aug 2026 09:12:12 -0500 Subject: [PATCH 34/71] ai(rules[AGENTS]) Judge comments by three gates why: Two rules in this file gave opposite answers on the same comment. The slop rubric said to keep the text and ask when unsure; the estate now prefers deletion in the borderline case. what: - Add "Comments earn their maintenance cost": the loss, elite, and upkeep gates, the one-to-two-line ceiling, and the keep and delete lists - Exempt doctests, usage examples, and param, return, and raises lines on public API from the loss gate, and from nothing else - Point "Preservation & Context" at the new rule and drop its keep-when-unsure default, keeping the invariant carve-out --- AGENTS.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e44391e3..471e4b93 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -514,6 +514,90 @@ When stuck in debugging loops: - FastMCP: https://github.com/jlowin/fastmcp - MCP Specification: https://modelcontextprotocol.io/ +## Comments earn their maintenance cost + +A comment ships only if it passes all three gates. Fail any: delete or rewrite. +Borderline: delete — borderline means the information is reconstructible, which +is what makes deletion cheap. + +**Loss.** Three years from now, would losing this cost a maintainer real time +rediscovering intent, an invariant, a constraint, or a failure mode the code and +tests do not already make obvious? + +**Elite.** Would SQLite, Redis, the Go standard library, or CPython write this +comment, at this length? Those projects state the constraint and stop. They do +not argue with an imagined objector. + +**Upkeep.** Will it stay true without maintenance? A comment that hand-syncs a +value the code owns — a count, an offset, a line reference, a duplicated +constant — is false the first time that value moves. + +### Ceiling + +One or two lines. A comment reaching four is either carrying several facts, in +which case split it, or arguing, in which case cut it to the fact. + +Rationale, alternatives weighed, and the story of how the code got here belong +in the commit message: timestamped, attached to the exact diff, and free to +maintain. + +A comment often holds both a constraint and the deliberation that found it. Keep +the constraint, cut the deliberation. "Runs at most once per second" survives; +"this is the right trade for now" does not. + +### Keep + +- Why over how: upstream quirks, protocol and compatibility constraints, + performance tradeoffs still part of the contract. +- Invariants, preconditions, ordering, lifetime, and concurrency requirements + that types and tests cannot express. +- Code that looks wrong but is not, so a later cleanup does not reintroduce the + bug. +- A high-level sketch of an algorithm whose local operations do not reveal the + whole. + +### Delete + +- Narration of the next lines; code translated into English. +- Restated names, types, defaults, or control flow. +- Values duplicated from the code and hand-synced. +- Justification, hedging, or apology for a choice. +- Speculation about future requirements. +- History version control already holds, including commented-out code. +- Ticket and issue numbers. They say nothing to a reader without tracker access, + and they rot when the tracker moves. Unfinished work goes in the tracker, not + the source. +- Transient observations — "currently", "for now", "the latest release" — + that go stale with no nearby edit. + +### The upkeep gate in practice + +It reaches values that track our own code. It does not reach frozen external +facts. + +Bad (Delete): + +```python +# There are 321 tests to complete for servers. +``` + +Good (Keep): + +```python +# tmux < 3.2 reports the pane ID only after the command completes, +# so this query must stay separate. +``` + +### Documentation exception + +Doctests, minimal usage examples, and param, return, and raises lines on public +API are exempt from the loss gate — they serve the caller, not the maintainer. +They are exempt from nothing else. Ceiling: a good man page entry. + +NumPy-style `Parameters`, `Returns`, and `Attributes` sections and executable +doctests fall under this exception — autodoc ships every field whether or not +you describe it, and a doctest that runs is also a test. + ## AI Slop Prevention Treat AI slop as **review-hostile noise**, not as proof that text or @@ -571,8 +655,9 @@ on unrelated code while still resolving. ### Preservation & Context -**When unsure, leave the text in place and ask.** Subjective cleanup -must never be a reason to remove load-bearing rationale. +Subjective cleanup must never remove load-bearing rationale. Adjudicate +comments with the comment policy above; borderline cases are deleted, not +kept. - **Preserve the "Why":** You MUST NOT delete comments that document invariants, protocol constraints, platform quirks, security From 7228f13405b86d2bfd4d467af57402e5a1073450 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 04:43:09 -0500 Subject: [PATCH 35/71] .tool-versions(uv) uv 0.12.3 -> 0.12.5 why: 0.12.5 ships CPython 3.10.21, 3.11.16 and 3.12.14. This repository series-pins 3.10 through 3.14 rather than naming patch levels, so those interpreters are simply what the existing pins now resolve to. 0.12.4 preserves consecutive wildcard Python minor-version exclusions such as `!=3.11.*, !=3.12.*` when rewriting uv.lock, coalesces gaps in the resolver's version ranges, and adds post-quantum TLS key exchange. See also: - https://github.com/astral-sh/uv/releases/tag/0.12.4 - https://github.com/astral-sh/uv/releases/tag/0.12.5 --- .tool-versions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tool-versions b/.tool-versions index 823ef64f..69ad986d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ just 1.58.0 -uv 0.12.3 +uv 0.12.5 python 3.14 3.13 3.12 3.11 3.10 From ba40c5ebb439709db5df38f0198677d30fd9f80c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 04:53:19 -0500 Subject: [PATCH 36/71] py(deps[dev]) ruff 0.16.2 -> 0.16.3 why: Reviewed against this repository and inert. 0.16.3 changes S602, S603, S607 and S609 to also inspect keyword arguments; the only flake8-bandit rules selected here are S102, none of which changed. UP048 and the PLR6104 false-negative fix are preview-only and preview mode is off in this project. What does land is a faster binary: 0.16.3 builds Linux x86-64 with PGO and shrinks `Expr` to 64 bytes. what: - uv.lock: ruff 0.16.2 -> 0.16.3 (floor `ruff>=0.16.1` unchanged) See also: https://github.com/astral-sh/ruff/releases/tag/0.16.3 --- uv.lock | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/uv.lock b/uv.lock index f9912b58..6021c710 100644 --- a/uv.lock +++ b/uv.lock @@ -2564,27 +2564,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, - { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, - { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, - { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, - { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, - { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, - { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, - { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, ] [[package]] From 22a687e1258fcb5cda4c479134a757466a69c1e4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 04:55:05 -0500 Subject: [PATCH 37/71] py(deps[dev]) mypy 2.3.0 -> 2.3.1 why: A patch release with no CHANGELOG entry, so the diff is the record: four commits, three of them mypyc runtime fixes (clearing a coroutine's environment on completion, `default_factory` on inherited dataclasses, and a crash on double-yielding Iterators) and one type-checker fix, a crash when unpacking the return value of an overload. This project runs mypy as a checker rather than compiling with mypyc, so the overload crash is the fix that reaches it. what: - uv.lock: mypy 2.3.0 -> 2.3.1 See also: - https://github.com/python/mypy/compare/v2.3.0...v2.3.1 - https://pypi.org/project/mypy/2.3.1/ --- uv.lock | 101 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/uv.lock b/uv.lock index 6021c710..b189f47a 100644 --- a/uv.lock +++ b/uv.lock @@ -1630,7 +1630,7 @@ wheels = [ [[package]] name = "mypy" -version = "2.3.0" +version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ast-serialize" }, @@ -1640,52 +1640,59 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { 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/a9/09/f2f5f45dae0c9a0891e4751a73312730e009395102e5d72a22a976cca41f/mypy-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc", size = 14927774, upload-time = "2026-07-13T11:28:38.224Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/345367effd3a6877275a94d481614bfca983f45e028c6290e2cc54603811/mypy-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3", size = 14000127, upload-time = "2026-07-13T11:30:19.57Z" }, - { url = "https://files.pythonhosted.org/packages/99/6c/a10b7a7b9f0a755fb94e27ae834d4cea9ad6c5221f9325eef8f182641feb/mypy-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805", size = 14229437, upload-time = "2026-07-13T11:28:17.765Z" }, - { url = "https://files.pythonhosted.org/packages/d9/bd/a26a602acb1bbf849fa4bdac4bc657ee2f11c0c2a764a2cc87a5304e865c/mypy-2.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117", size = 15171457, upload-time = "2026-07-13T11:29:01.834Z" }, - { url = "https://files.pythonhosted.org/packages/7f/14/124f462bef69bcbc90b9358088460b6091954a3e004852fcd9948db617a5/mypy-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5", size = 15478281, upload-time = "2026-07-13T11:32:23.413Z" }, - { url = "https://files.pythonhosted.org/packages/db/a4/8bdca6a8ac8d856d82ed049144af2721245a135c2e8001d3890c93975852/mypy-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494", size = 11148008, upload-time = "2026-07-13T11:34:17.332Z" }, - { url = "https://files.pythonhosted.org/packages/83/41/490eea348e60ba50decec20bc750605444149a5d7a8cc560042f90ba2c75/mypy-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee", size = 10142329, upload-time = "2026-07-13T11:32:52.116Z" }, - { 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/b9/de8f67e12d721cdcc8ba6cfc440b989a4ba4dfabe4402ae94dfdd8bb30a4/mypy-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57a936373fc690c43a8cd7e7e12a35148e4ec5aa7698ad7fc0a9f918bdc5be41", size = 14015541, upload-time = "2026-08-15T03:01:53.104Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8a/9e746ab012c67ed8ea3232a613716c306ee8c0b5682c80d8103b4f04568e/mypy-2.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d00d769056bde2f4e69c175071eba45cfb44fa1ed92bdfbfe64a93e0543b0cf0", size = 14248142, upload-time = "2026-08-15T03:02:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/f7/5c/c99ff2d8d0e2c53393e32dfe22d9aa43a5d959d30db46c786dafd24527d3/mypy-2.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2166b29228835e1f88ff411e96639e6ca3c7fdde84b62ec211f70f86b4051167", size = 15193309, upload-time = "2026-08-15T03:01:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/124638f745243faae1ff4b37d5426fe41c0f0454535edc82fe8102b56a3c/mypy-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83d36c2924df7426333abe7faf4724a7e1aab0d9fd41625e81b4683034b80c13", size = 15498246, upload-time = "2026-08-15T03:02:46.29Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/31c0781e243836505c0fb5f4e865487d6df1023e4ad959f4ebd4b84a0226/mypy-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:f12fdb70459d0060dea40b29e52163a961b156106d68d57882a6a9f648983a53", size = 11155028, upload-time = "2026-08-15T03:01:39.08Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ab/bc2eb0129e72d7d7d93d5e981a78084a9abefda7efa732a7e02f97d6e27d/mypy-2.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:e099200a1b1b1223a4951f0a90cbff1b8c91b250ba599dab1f7217a628144d90", size = 10151438, upload-time = "2026-08-15T03:02:19.04Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/c624d4241484f37dc62839e177ab607a9b8b3e96f0866544ca99e8e41d51/mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531", size = 13936739, upload-time = "2026-08-15T03:03:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/53/84/e3cf72f90dce5960871c82551c8fba6da05fc1018f79be41c047bd126bdd/mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8", size = 14166460, upload-time = "2026-08-15T03:01:50.565Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ff/6b97d58aa0f79a5ab9b472db1f6d6df1b11a51d74d0c08ab3760d3a613ba/mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8", size = 15100476, upload-time = "2026-08-15T03:03:12.079Z" }, + { url = "https://files.pythonhosted.org/packages/da/f0/cbb4b7d2ae3ac635f6b4f2d9b04070b8a92edf50da599d3b39e5ed109001/mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01", size = 15347826, upload-time = "2026-08-15T03:03:02.856Z" }, + { url = "https://files.pythonhosted.org/packages/5f/10/91dcdc6f8d43fc08e6a06ab1f9732f3abaaf835ac1b2e67b9dff56910855/mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1", size = 11142615, upload-time = "2026-08-15T03:03:36.316Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8a/28d54535bf4b9aa43b2d8918c2ef660378b9f66b23d78dcee052744ae622/mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6", size = 10141145, upload-time = "2026-08-15T03:03:07.406Z" }, + { url = "https://files.pythonhosted.org/packages/85/da/d6effc4f808a842d91edc22535dc9e799d2ff6e91449168b7f47a0771f54/mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff", size = 14047547, upload-time = "2026-08-15T03:02:57.707Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e6/478229701dab76f26485fc8ff5d6f241f393da22447400bbc56f6946aebe/mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff", size = 14216515, upload-time = "2026-08-15T03:01:26.496Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/7c42327a3b21e84681f691982cbfe43f334a3685f3b683b72c376476c4fa/mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080", size = 15307789, upload-time = "2026-08-15T03:03:31.62Z" }, + { url = "https://files.pythonhosted.org/packages/59/f4/7e597edbe01b5a56fa958ce541302dcaabfed979966f1dffedbea0ea0fc2/mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355", size = 15548831, upload-time = "2026-08-15T03:03:15.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/52/cb31e084bc0314a1e384bdd677a4b80e55af04ccac077545e2238b9d320a/mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb", size = 11226359, upload-time = "2026-08-15T03:03:29.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/47/88fcf6217b43fa2da81a8c2611370af18141536a4f0294bbf98b457d456d/mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b", size = 10214707, upload-time = "2026-08-15T03:02:48.807Z" }, + { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" }, + { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" }, + { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" }, + { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" }, + { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" }, + { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" }, + { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" }, + { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" }, ] [[package]] From e6cdf38ee9b0d5290befb87489f60496e23aeb90 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 04:58:31 -0500 Subject: [PATCH 38/71] py(deps[dev]) Bump dev packages --- uv.lock | 296 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 186 insertions(+), 110 deletions(-) diff --git a/uv.lock b/uv.lock index b189f47a..d1a05567 100644 --- a/uv.lock +++ b/uv.lock @@ -447,89 +447,165 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, - { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, - { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, - { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, - { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, - { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, - { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, - { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, - { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, - { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, - { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, - { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, - { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, - { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, - { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, - { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, - { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, - { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, - { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, - { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, - { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, - { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, - { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, - { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, - { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, - { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, - { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, - { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, - { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, - { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, - { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, - { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, - { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, - { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, - { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, - { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, - { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, - { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, - { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, - { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, - { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, - { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" }, + { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" }, + { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" }, + { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] @@ -758,7 +834,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "4.22.5" +version = "4.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -768,9 +844,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/05/689617b7e86503417c172f577d791524cb13b9697303d5d44409a971ba10/cyclopts-4.22.5.tar.gz", hash = "sha256:94044506317462cad90fb01a917dadce1f48a0915ba3605dc8d178dea1229e24", size = 195144, upload-time = "2026-08-04T13:53:00.303Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/62/1b160d5e8c20174392a3a5e3e7e6542e02e6f6922b35ba0962829a6b5c90/cyclopts-4.23.0.tar.gz", hash = "sha256:2f764bbd90f1888073971c09576f90e594f80353588e10aa615b7d59bc009821", size = 195257, upload-time = "2026-08-17T19:44:21.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/58/bcab9c33fb7a25a1f5970f357c5b19729bc81d50615d2f737b20c4255909/cyclopts-4.22.5-py3-none-any.whl", hash = "sha256:cf9ce285836053d156730ea4ea0ad0c75cf63beb3f3d8edf222a795bc57666ab", size = 234557, upload-time = "2026-08-04T13:52:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/32/1f/70aeb4f9a420cb62726d943b0d893c2bf9e9ab9c81a93f7542d3beb5d69c/cyclopts-4.23.0-py3-none-any.whl", hash = "sha256:1581758c5b9982c3b2ae7df6d87243e3a6fc3265ea621f96caeb349b6fa43ad2", size = 234671, upload-time = "2026-08-17T19:44:19.604Z" }, ] [[package]] @@ -960,11 +1036,11 @@ wheels = [ [[package]] name = "griffelib" -version = "2.1.0" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, ] [[package]] @@ -1015,11 +1091,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -1801,11 +1877,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.11.2" +version = "4.11.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/98/0bf930c4f97d0266b58a89e36c015f56232c52b5d2f207215d48cca9e8f7/platformdirs-4.11.2.tar.gz", hash = "sha256:3a2ae5fca3520a01ab1be8b45613537f52ddf5b5f6f53d88233892dfbf0cd82d", size = 32716, upload-time = "2026-08-10T15:48:06.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e2/4e6eee633809c376c024821b91ade709cbfd040ec53939ffbcc292aa7eee/platformdirs-4.11.2-py3-none-any.whl", hash = "sha256:7f89089b6ea71bda7962953edcf784b2e2d9d285b40ad88be2bb75c6e9d82ab4", size = 23361, upload-time = "2026-08-10T15:48:04.855Z" }, + { url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" }, ] [[package]] @@ -2004,11 +2080,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] @@ -2083,15 +2159,15 @@ wheels = [ [[package]] name = "pytest-rerunfailures" -version = "16.4" +version = "16.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/60/a90ca1cc6cffcb97b4260ed0ad2b7934b999d7c48abe4ea0840344862a3b/pytest_rerunfailures-16.4.tar.gz", hash = "sha256:8222d17c37eb7b9e4d6fc96a3c724ff4e1a5c97a5cc7cbb2c19e9282cfd21a11", size = 36635, upload-time = "2026-07-01T06:30:56.813Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/0114e45d4b2fcd5f6297dac655c067b47de28be4d33e088f200f0f2c4c28/pytest_rerunfailures-16.6.tar.gz", hash = "sha256:29dbfee46f542073c888e0ed4e81c51e15b9096f49a299eb1a759629c601684a", size = 42806, upload-time = "2026-08-17T07:11:00.447Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/93/3cdcc4033444e822e01b573414b03fd37fd5533070c750477b8f5fa5224b/pytest_rerunfailures-16.4-py3-none-any.whl", hash = "sha256:f69b5beb39622c90d1e44bd945d826eff6db545dcf0b68f52b7e4ad15eaf6d6c", size = 16955, upload-time = "2026-07-01T06:30:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/1e994889673d7a0da11651f17ef789b6c83bfe349f29f871873dd3802445/pytest_rerunfailures-16.6-py3-none-any.whl", hash = "sha256:6af2d1ebd6e5cb79666ac408942cd6a0672a49fefd8523664770718560795e13", size = 19137, upload-time = "2026-08-17T07:10:59.121Z" }, ] [[package]] @@ -2122,11 +2198,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.2" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, ] [[package]] @@ -3123,14 +3199,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.3" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/bc/4eae18cd40c65798a16267572ba346c11f599d44b01603dbd843342042bc/typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d", size = 76711, upload-time = "2026-08-10T09:39:18.063Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/f7/7a3935abdebd5cf18705a5f0335dd6a3a18bef3baa7cb9edc3b6b9922cc8/typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd", size = 14693, upload-time = "2026-08-10T09:39:16.693Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] [[package]] @@ -3162,16 +3238,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.52.1" +version = "0.52.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/18/ccce41535dee1be77735592bd19965f3972c82e07ee703d324709496b716/uvicorn-0.52.1.tar.gz", hash = "sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd", size = 100571, upload-time = "2026-08-01T18:19:30.732Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859, upload-time = "2026-08-01T18:19:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, ] [[package]] From 34860b2416fe2ace33d8e5b321dc531fc33a464d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 06:05:25 -0500 Subject: [PATCH 39/71] tests(docs) Re-record the JSON highlight snapshot why: pygments 2.21.0 makes HtmlFormatter render `"` and `'` literally instead of `"` and `'` (pygments#3185), so the recorded fragment for the uvx MCP config stopped matching. This lands as its own commit because the dev-package refresh that raised pygments does not mention it: the failure surfaced after that commit was already pushed. Comparing the two revisions line by line, every difference is exactly that one substitution - same tokens, same span classes, same whitespace, four lines. Rendered output is unchanged, since inside element text content `"` and `"` are equivalent; the escaping was only ever required inside attribute values. Verified by pinning pygments back to 2.20.0, where the old snapshot passes, and forward to 2.21.0, where only this substitution differs. what: - Re-record highlight_json-mcp-config-uvx against pygments 2.21.0 See also: https://github.com/pygments/pygments/issues/3185 --- tests/docs/__snapshots__/test_widgets.ambr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/docs/__snapshots__/test_widgets.ambr b/tests/docs/__snapshots__/test_widgets.ambr index 18970ef3..78f73e47 100644 --- a/tests/docs/__snapshots__/test_widgets.ambr +++ b/tests/docs/__snapshots__/test_widgets.ambr @@ -16,10 +16,10 @@ # name: test_highlight_filter_matches_sphinx_native[json-mcp-config-uvx][highlight_json-mcp-config-uvx] '''
{
-      "mcpServers": {
-          "tmux": {
-              "command": "uvx",
-              "args": ["libtmux-mcp"]
+      "mcpServers": {
+          "tmux": {
+              "command": "uvx",
+              "args": ["libtmux-mcp"]
           }
       }
   }

From e7a46fa61321cebf0f582a4fb09888930485f8bf Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Sat, 22 Aug 2026 09:23:41 -0500
Subject: [PATCH 40/71] ci(dependabot): stop filing github-actions updates

why: Action pins in this repository are maintained by a researched sweep that
reads every `uses:` line from trunk, verifies each target tag resolves, and
lands one commit per action with the upstream release notes and the migration
impact recorded in the message. Dependabot's pull requests duplicate that work
with a bot-authored commit message, and the notification volume across the
estate is not worth what they add.

what:
- deleted .github/dependabot.yml (github-actions was its only ecosystem)
- Dependency updates for every other ecosystem continue unchanged
---
 .github/dependabot.yml | 7 -------
 1 file changed, 7 deletions(-)
 delete mode 100644 .github/dependabot.yml

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index d202a332..00000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-version: 2
-updates:
-  - package-ecosystem: "github-actions"
-    directory: "/"
-    schedule:
-      # Check for updates to GitHub Actions every week
-      interval: "weekly"

From c2017275ee99d628183e80a4d87febc186af7569 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Sat, 22 Aug 2026 09:26:35 -0500
Subject: [PATCH 41/71] ci(deps): bump astral-sh/setup-uv from v9.0.0 to
 v10.0.1

why: v10.0.0 is a deliberate breaking release one step after v9.0.0; upstream's own
note is "Another breaking release, directly after v9.0.0 but we think the added
security justifies that." It adds a `version: "latest-known"` selector that
installs the newest uv release with a known checksum, and reads a Python version
from a referenced `.tool-versions` file. v10.0.1 is a bug-fix patch only,
tolerating a transient manifest timeout.

The pin is the exact tag rather than a floating major on purpose: this action
stopped publishing major tags after v7, so `astral-sh/setup-uv@v10` does not
resolve. No inputs were added, removed or renamed across the span.

v10 also changes the cache default: under `enable-cache: auto` it now disables
caching for pull_request_target, workflow_run, release and tag-push triggers to
close a cache-poisoning path (astral-sh/setup-uv#984). This repository does run
setup-uv on release, but every one of its setup-uv steps sets
`enable-cache: true` explicitly, which overrides the default - so caching
behaviour here is unchanged.

Releases:
- https://github.com/astral-sh/setup-uv/releases/tag/v10.0.0
- https://github.com/astral-sh/setup-uv/releases/tag/v10.0.1
---
 .github/workflows/docs.yml  | 2 +-
 .github/workflows/tests.yml | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 2c76adf1..2f067317 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -40,7 +40,7 @@ jobs:
 
       - name: Install uv
         if: env.PUBLISH == 'true'
-        uses: astral-sh/setup-uv@v9.0.0
+        uses: astral-sh/setup-uv@v10.0.1
         with:
           enable-cache: true
 
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ace9b38d..b7394e9c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -16,7 +16,7 @@ jobs:
       - uses: actions/checkout@v7
 
       - name: Install uv
-        uses: astral-sh/setup-uv@v9.0.0
+        uses: astral-sh/setup-uv@v10.0.1
         with:
           enable-cache: true
 
@@ -102,7 +102,7 @@ jobs:
       - uses: actions/checkout@v7
 
       - name: Install uv
-        uses: astral-sh/setup-uv@v9.0.0
+        uses: astral-sh/setup-uv@v10.0.1
         with:
           enable-cache: true
 

From 517d0da4c01f01ba79267e794be301ae2ddc4250 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 19:41:34 -0500
Subject: [PATCH 42/71] Mcp(fix[safety]): Name the tier a gated tool needs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Calling a tool above the server's `LIBTMUX_SAFETY` tier reported
`Unknown tool: 'kill_pane'` — the server denying that its own gated
tool exists. An agent told a tool is absent reports the capability as
missing instead of naming the setting that enables it.

Two gates were intended and only one worked. FastMCP's native
`disable()` enforces the tier; `SafetyMiddleware` was meant to explain
it. `get_tool()` answers `None` for a disabled tool, so the guard
reading `if tool and not allowed` never ran for precisely the tools it
was written for, and dispatch raised `NotFoundError` instead. Off-tier
names now resolve against `_list_tools()`, which retains disabled tools
with their tags.

The batch wrappers carried the same defect through a second call path:
`_get_allowed_tool_tier` also read `get_tool` and raised "Unknown tool"
on `None`, so a gated tool and a misspelled one produced byte-identical
rows. It hands the operation on instead of duplicating the lookup —
nested calls already run with `run_middleware=True`, so one source of
truth decides which of the two it is.

The message also hardcoded `LIBTMUX_SAFETY=destructive` for every
denial, so a readonly server answered a `send_keys` call by advising the
strongest tier — telling a user to grant `kill_server` rights in order
to type into a pane. Denials now name the required tier and the tier in
force.

Restores an audit property the middleware ordering is built around: a
tier denial must raise inside `SafetyMiddleware` so `AuditMiddleware`,
sitting outside it, records it as a denial. While the denial never
fired, blocked calls were audited as unknown-tool errors.

`on_call_tool` now fails closed with no FastMCP context. It previously
fell through to the tool, which is fail-open in a gate whose top tier
includes `kill_server`, and was masked only because the native gate made
the dispatch fail anyway.

The existing suite documented the dead path rather than testing it, so
921 passing tests never called a gated tool end to end. Adds that
coverage per tier and through the batch wrapper, plus a contract test
pinning the private `_list_tools()` behavior the explanation depends on.
---
 CHANGES                              |  49 ++++++++
 src/libtmux_mcp/middleware.py        | 121 ++++++++++++++++--
 src/libtmux_mcp/server.py            |   7 +-
 src/libtmux_mcp/tools/batch_tools.py |  10 +-
 tests/test_middleware.py             | 175 ++++++++++++++++++++++++++
 tests/test_server.py                 | 182 +++++++++++++++++++++++++++
 6 files changed, 528 insertions(+), 16 deletions(-)

diff --git a/CHANGES b/CHANGES
index b40708cb..7ba3f7b2 100644
--- a/CHANGES
+++ b/CHANGES
@@ -6,6 +6,55 @@
 _Notes on upcoming releases will be added here_
 
 
+### What's new
+
+#### A gated tool now says which tier it needs
+
+Calling a tool above the server's `LIBTMUX_SAFETY` tier reported
+`Unknown tool: 'kill_pane'` — the server denying that its own gated tool
+exists. An agent told a tool is absent reports the capability as missing
+rather than naming the setting that enables it.
+
+Off-tier calls now name both the tier the tool requires and the tier in
+force:
+
+```
+Tool 'kill_pane' requires safety level 'destructive', but this server is
+running at 'mutating'. Restart it with LIBTMUX_SAFETY=destructive to
+enable it.
+```
+
+The message previously hardcoded `LIBTMUX_SAFETY=destructive` for every
+denial, so a `readonly` server answered a `send_keys` call by advising
+the strongest tier — telling a user to grant `kill_server` rights in
+order to type into a pane. The tier named is now the one the tool
+actually needs.
+
+Two gates were always intended here, and only one of them worked.
+FastMCP's native `disable()` enforces the tier; `SafetyMiddleware` was
+meant to explain it. Because `get_tool()` answers `None` for a disabled
+tool, the middleware's `if tool and not allowed` guard never ran for the
+tools it was written for. Off-tier names now resolve against the full
+registry, which retains disabled tools and their tags.
+
+Denials also reach the audit log as denials again. The server's
+middleware ordering is built so that a tier denial raises inside
+`SafetyMiddleware` and is recorded by `AuditMiddleware` outside it;
+while the denial never fired, a blocked call was audited as an
+unknown-tool error instead.
+
+The batch wrappers carried the same defect through a second call path:
+`_get_allowed_tool_tier` also read `get_tool` and raised "Unknown tool"
+on `None`, so a gated tool and a misspelled one produced byte-identical
+rows. It now hands the operation on instead of duplicating the lookup —
+the nested call runs through the middleware, which names the tier for a
+gated tool while a real typo still gets FastMCP's own error.
+
+`SafetyMiddleware.on_call_tool` additionally fails closed when no
+FastMCP context is present. It previously fell through to the tool,
+which was fail-open in a gate whose top tier includes `kill_server`, and
+was masked only because the native gate made the dispatch fail anyway.
+
 ### Documentation
 
 #### opencode joins the install picker
diff --git a/src/libtmux_mcp/middleware.py b/src/libtmux_mcp/middleware.py
index 9226bf2a..bde3be84 100644
--- a/src/libtmux_mcp/middleware.py
+++ b/src/libtmux_mcp/middleware.py
@@ -51,25 +51,53 @@
     ExpectedToolError,
 )
 
+logger = logging.getLogger(__name__)
+
 _TIER_LEVELS: dict[str, int] = {
     TAG_READONLY: 0,
     TAG_MUTATING: 1,
     TAG_DESTRUCTIVE: 2,
 }
 
+#: Reverse of :data:`_TIER_LEVELS`, so a middleware configured with an
+#: unrecognized tier can still *name* the tier it fell back to.
+_LEVEL_TIERS: dict[int, str] = {level: tier for tier, level in _TIER_LEVELS.items()}
+
+
+def _highest_tier(tags: t.Collection[str]) -> str | None:
+    """Return the highest safety tier named in *tags*, or None if untagged."""
+    found = [tier for tier in _TIER_LEVELS if tier in tags]
+    if not found:
+        return None
+    return max(found, key=lambda tier: _TIER_LEVELS[tier])
+
 
 class SafetyMiddleware(Middleware):
-    """Gate tools by safety tier.
+    """Explain tier denials that ``_enable_allowed_tools`` enforces.
+
+    FastMCP's native ``disable()`` is the enforcement gate and holds
+    even for a call that skips the middleware chain. It also makes
+    ``get_tool()`` answer **None**, so FastMCP reports an off-tier call
+    as ``Unknown tool`` -- the server denying its own gated tool exists.
+    This gate names the required tier instead, resolving such names
+    against :meth:`_tier_snapshot` (the registry keeps disabled tools).
+
+    Denials must raise *here* so :class:`AuditMiddleware`, which sits
+    outside, records them as denials rather than unknown-tool errors.
 
     Parameters
     ----------
     max_tier : str
-        Maximum allowed tier. One of ``TAG_READONLY``, ``TAG_MUTATING``,
-        or ``TAG_DESTRUCTIVE``.
+        Maximum allowed tier. Unrecognized values fall back to
+        ``TAG_READONLY``.
     """
 
     def __init__(self, max_tier: str = TAG_MUTATING) -> None:
         self.max_level = _TIER_LEVELS.get(max_tier, 0)
+        #: Normalized tier name, so a denial reports where the server
+        #: stands rather than echoing an unrecognized env value back.
+        self.max_tier = _LEVEL_TIERS[self.max_level]
+        self._tier_by_tool: dict[str, str] | None = None
 
     def _is_allowed(self, tags: set[str]) -> bool:
         """Return True if the tool's tags fall within the allowed tier.
@@ -84,6 +112,55 @@ def _is_allowed(self, tags: set[str]) -> bool:
                     return False
         return found_tier
 
+    def _denial_message(self, tool_name: str, required_tier: str | None) -> str:
+        """Name the required tier and the active one.
+
+        The message this replaced hardcoded ``destructive`` for every
+        denial, so a readonly server answered ``send_keys`` by advising
+        kill_server rights in order to type into a pane.
+        """
+        if required_tier is None:
+            return (
+                f"Tool {tool_name!r} declares no safety tier and is blocked. "
+                "This is a bug in the server, not a configuration problem; "
+                "please report it."
+            )
+        return (
+            f"Tool {tool_name!r} requires safety level {required_tier!r}, but "
+            f"this server is running at {self.max_tier!r}. Restart it with "
+            f"LIBTMUX_SAFETY={required_tier} to enable it."
+        )
+
+    async def _tier_snapshot(self, fastmcp: t.Any) -> dict[str, str]:
+        """Map every registered tool name to its tier, disabled included.
+
+        Built once. ``_list_tools()`` is FastMCP-private, so a failure
+        degrades to an empty map: the call then falls through to the
+        stock ``NotFoundError``, losing the explanation but nothing
+        else. ``tests/test_server.py`` pins the behavior so a fastmcp
+        bump fails in CI rather than silently reverting this gate.
+        """
+        if self._tier_by_tool is not None:
+            return self._tier_by_tool
+
+        snapshot: dict[str, str] = {}
+        try:
+            registered = await fastmcp._list_tools()
+        except Exception:
+            logger.warning(
+                "safety tier snapshot unavailable; off-tier calls will "
+                "report 'unknown tool'",
+                exc_info=True,
+            )
+        else:
+            for tool in registered:
+                tier = _highest_tier(tool.tags)
+                if tier is not None:
+                    snapshot[tool.name] = tier
+
+        self._tier_by_tool = snapshot
+        return snapshot
+
     async def on_list_tools(
         self,
         context: MiddlewareContext,
@@ -98,16 +175,36 @@ async def on_call_tool(
         context: MiddlewareContext,
         call_next: t.Any,
     ) -> t.Any:
-        """Block execution of tools above the safety tier."""
-        if context.fastmcp_context:
-            tool = await context.fastmcp_context.fastmcp.get_tool(context.message.name)
-            if tool and not self._is_allowed(tool.tags):
-                msg = (
-                    f"Tool '{context.message.name}' is not available at the "
-                    f"current safety level. Set LIBTMUX_SAFETY=destructive "
-                    f"to enable destructive tools."
+        """Block execution of tools above the safety tier.
+
+        Fail-closed except for a name the registry has never heard of,
+        which is a typo and deserves FastMCP's own ``NotFoundError``.
+        """
+        tool_name = context.message.name
+
+        if context.fastmcp_context is None:
+            # No registry to consult: deny, since the top tier includes
+            # kill_server.
+            msg = (
+                f"Tool {tool_name!r} was called without a FastMCP context, so "
+                "its safety tier cannot be verified. Call it through MCP."
+            )
+            raise ExpectedToolError(msg)
+
+        fastmcp = context.fastmcp_context.fastmcp
+
+        tool = await fastmcp.get_tool(tool_name)
+        if tool is not None:
+            if not self._is_allowed(tool.tags):
+                raise ExpectedToolError(
+                    self._denial_message(tool_name, _highest_tier(tool.tags))
                 )
-                raise ExpectedToolError(msg)
+            return await call_next(context)
+
+        # Invisible to ``get_tool``: gated by tier, or nonexistent.
+        gated_tier = (await self._tier_snapshot(fastmcp)).get(tool_name)
+        if gated_tier is not None:
+            raise ExpectedToolError(self._denial_message(tool_name, gated_tier))
         return await call_next(context)
 
 
diff --git a/src/libtmux_mcp/server.py b/src/libtmux_mcp/server.py
index c5772f1b..d7934586 100644
--- a/src/libtmux_mcp/server.py
+++ b/src/libtmux_mcp/server.py
@@ -436,8 +436,11 @@ def _enable_allowed_tools() -> None:
     if _mcp_visibility_configured:
         return
 
-    # Use FastMCP's native visibility system as primary gate,
-    # with the SafetyMiddleware as a secondary layer for clear error messages.
+    # This is the ENFORCEMENT gate: it holds even for a call that skips
+    # the middleware chain (``call_tool`` accepts
+    # ``run_middleware=False``). ``SafetyMiddleware`` is the
+    # EXPLANATION gate -- disabling makes ``get_tool`` answer None, so
+    # FastMCP would otherwise report a gated tool as ``Unknown tool``.
     allowed_tags = {TAG_READONLY}
     if _safety_level in {TAG_MUTATING, TAG_DESTRUCTIVE}:
         allowed_tags.add(TAG_MUTATING)
diff --git a/src/libtmux_mcp/tools/batch_tools.py b/src/libtmux_mcp/tools/batch_tools.py
index 2091f4b4..2b52780e 100644
--- a/src/libtmux_mcp/tools/batch_tools.py
+++ b/src/libtmux_mcp/tools/batch_tools.py
@@ -125,8 +125,14 @@ async def _get_allowed_tool_tier(
 
     tool = await fastmcp.get_tool(operation.tool)
     if tool is None:
-        msg = f"Unknown tool: {operation.tool!r}"
-        raise ExpectedToolError(msg)
+        # None means nonexistent OR disabled by tier, so raising
+        # "Unknown tool" here denied that a gated tool exists. Hand it
+        # on instead: the nested call runs with ``run_middleware=True``,
+        # letting ``SafetyMiddleware`` name the tier and FastMCP still
+        # raise ``NotFoundError`` for a typo. Nothing is skipped --
+        # visibility follows tier tags, so an invisible tool is
+        # off-tier by construction and is denied before these checks.
+        return
 
     # ``max_tier`` is a CEILING, so a readonly tool is reachable through
     # every batch wrapper, not only the readonly one. The batch loop is
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 495e1532..a20d2458 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -134,6 +134,181 @@ def test_safety_middleware_invalid_tier_falls_back() -> None:
     assert mw._is_allowed({TAG_DESTRUCTIVE}) is False
 
 
+# ---------------------------------------------------------------------------
+# SafetyMiddleware — off-tier denial messages.
+#
+# Fast unit coverage against fake registries; ``tests/test_server.py``
+# drives the same paths through a real server process.
+# ---------------------------------------------------------------------------
+
+
+def _fake_fastmcp(
+    *,
+    visible: dict[str, set[str]] | None = None,
+    registered: dict[str, set[str]] | None = None,
+    list_tools_error: Exception | None = None,
+) -> t.Any:
+    """Build a stand-in FastMCP exposing name -> tags for both lookups.
+
+    *visible* is what ``get_tool`` resolves (enabled tools only, matching
+    FastMCP's real behavior of returning None for a disabled tool).
+    *registered* is what ``_list_tools`` returns (every tool, disabled
+    included) — the asymmetry that the fix depends on.
+    """
+
+    class _Tool:
+        def __init__(self, name: str, tags: set[str]) -> None:
+            self.name = name
+            self.tags = tags
+
+    visible_tools = {n: _Tool(n, tags) for n, tags in (visible or {}).items()}
+    all_tools = [_Tool(n, tags) for n, tags in (registered or {}).items()]
+
+    class _FastMCP:
+        async def get_tool(self, name: str) -> t.Any:
+            return visible_tools.get(name)
+
+        async def _list_tools(self) -> list[t.Any]:
+            if list_tools_error is not None:
+                raise list_tools_error
+            return all_tools
+
+    return _FastMCP()
+
+
+def _call_context(tool_name: str, fastmcp: t.Any) -> t.Any:
+    """Build a MiddlewareContext-alike for ``on_call_tool``."""
+    fastmcp_ctx = type("_Ctx", (), {"fastmcp": fastmcp})()
+    return type(
+        "_MW",
+        (),
+        {
+            "message": CallToolRequestParams(name=tool_name, arguments={}),
+            "fastmcp_context": fastmcp_ctx,
+        },
+    )()
+
+
+async def _unreachable(_ctx: t.Any) -> t.Any:
+    """``call_next`` that fails the test if the gate lets a call through."""
+    msg = "call_next must not run for an off-tier tool"
+    raise AssertionError(msg)
+
+
+def test_safety_denial_names_required_and_current_tier() -> None:
+    """A gated tool reports the tier it needs and the tier in force.
+
+    The replaced message hardcoded ``destructive`` for every denial, so
+    a readonly server answered ``send_keys`` by advising kill_server
+    rights in order to type into a pane.
+    """
+    mw = SafetyMiddleware(max_tier=TAG_READONLY)
+    fastmcp = _fake_fastmcp(
+        visible={},
+        registered={"send_keys": {TAG_MUTATING}},
+    )
+
+    with pytest.raises(ToolError) as excinfo:
+        asyncio.run(mw.on_call_tool(_call_context("send_keys", fastmcp), _unreachable))
+
+    message = str(excinfo.value)
+    assert "'mutating'" in message
+    assert "'readonly'" in message
+    assert "LIBTMUX_SAFETY=mutating" in message
+    assert "destructive" not in message
+
+
+def test_safety_denial_reaches_disabled_tools() -> None:
+    """A tool hidden by the native gate still gets a tier explanation.
+
+    ``get_tool`` returns None for a disabled tool, so the denial must
+    come from the registry snapshot or the agent is told it is unknown.
+    """
+    mw = SafetyMiddleware(max_tier=TAG_MUTATING)
+    fastmcp = _fake_fastmcp(
+        visible={"send_keys": {TAG_MUTATING}},
+        registered={"send_keys": {TAG_MUTATING}, "kill_pane": {TAG_DESTRUCTIVE}},
+    )
+
+    with pytest.raises(ToolError) as excinfo:
+        asyncio.run(mw.on_call_tool(_call_context("kill_pane", fastmcp), _unreachable))
+
+    assert "requires safety level 'destructive'" in str(excinfo.value)
+
+
+def test_safety_passes_through_genuinely_unknown_tool() -> None:
+    """An unregistered name is a typo and keeps FastMCP's own error."""
+    mw = SafetyMiddleware(max_tier=TAG_MUTATING)
+    fastmcp = _fake_fastmcp(visible={}, registered={"send_keys": {TAG_MUTATING}})
+    reached = []
+
+    async def _call_next(_ctx: t.Any) -> str:
+        reached.append("yes")
+        return "dispatched"
+
+    result = asyncio.run(
+        mw.on_call_tool(_call_context("sned_keys", fastmcp), _call_next)
+    )
+
+    assert result == "dispatched"
+    assert reached == ["yes"]
+
+
+def test_safety_denies_when_context_is_missing() -> None:
+    """No FastMCP context means no way to prove the tier: deny.
+
+    The previous guard fell through to ``call_next``, fail-OPEN in a
+    gate whose top tier includes ``kill_server``.
+    """
+    mw = SafetyMiddleware(max_tier=TAG_READONLY)
+    context = type(
+        "_MW",
+        (),
+        {
+            "message": CallToolRequestParams(name="kill_server", arguments={}),
+            "fastmcp_context": None,
+        },
+    )()
+
+    with pytest.raises(ToolError, match="safety tier cannot be verified"):
+        asyncio.run(mw.on_call_tool(context, _unreachable))
+
+
+def test_safety_snapshot_failure_degrades_to_passthrough(
+    caplog: pytest.LogCaptureFixture,
+) -> None:
+    """A broken private API costs the explanation, never the server.
+
+    If a fastmcp bump removes ``_list_tools``, the denial degrades to
+    the stock error rather than raising on every call.
+    """
+    mw = SafetyMiddleware(max_tier=TAG_READONLY)
+    fastmcp = _fake_fastmcp(
+        visible={},
+        list_tools_error=AttributeError("no attribute '_list_tools'"),
+    )
+
+    async def _call_next(_ctx: t.Any) -> str:
+        return "dispatched"
+
+    with caplog.at_level(logging.WARNING):
+        result = asyncio.run(
+            mw.on_call_tool(_call_context("kill_pane", fastmcp), _call_next)
+        )
+
+    assert result == "dispatched"
+    assert "safety tier snapshot unavailable" in caplog.text
+
+
+def test_safety_denies_tool_with_no_tier_tag() -> None:
+    """An untagged but visible tool stays fail-closed, with a bug hint."""
+    mw = SafetyMiddleware(max_tier=TAG_DESTRUCTIVE)
+    fastmcp = _fake_fastmcp(visible={"mystery": set()}, registered={"mystery": set()})
+
+    with pytest.raises(ToolError, match="declares no safety tier"):
+        asyncio.run(mw.on_call_tool(_call_context("mystery", fastmcp), _unreachable))
+
+
 # ---------------------------------------------------------------------------
 # AuditMiddleware
 # ---------------------------------------------------------------------------
diff --git a/tests/test_server.py b/tests/test_server.py
index 43218da3..e58ce70c 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -208,6 +208,188 @@ async def main():
     }
 
 
+class GatedToolFixture(t.NamedTuple):
+    """Test fixture for off-tier tool calls against a real server."""
+
+    test_id: str
+    safety: str
+    tool: str
+    required_tier: str
+
+
+GATED_TOOL_FIXTURES: list[GatedToolFixture] = [
+    GatedToolFixture(
+        test_id="readonly_denies_send_keys_as_mutating",
+        safety="readonly",
+        tool="send_keys",
+        required_tier="mutating",
+    ),
+    GatedToolFixture(
+        test_id="readonly_denies_kill_pane_as_destructive",
+        safety="readonly",
+        tool="kill_pane",
+        required_tier="destructive",
+    ),
+    GatedToolFixture(
+        test_id="mutating_denies_kill_pane_as_destructive",
+        safety="mutating",
+        tool="kill_pane",
+        required_tier="destructive",
+    ),
+    GatedToolFixture(
+        test_id="mutating_denies_destructive_batch",
+        safety="mutating",
+        tool="call_destructive_tools_batch",
+        required_tier="destructive",
+    ),
+]
+
+
+@pytest.mark.parametrize(
+    GatedToolFixture._fields,
+    GATED_TOOL_FIXTURES,
+    ids=[fixture.test_id for fixture in GATED_TOOL_FIXTURES],
+)
+def test_gated_tool_call_explains_the_tier(
+    test_id: str,
+    safety: str,
+    tool: str,
+    required_tier: str,
+) -> None:
+    """Calling an off-tier tool names the tier, not "unknown tool".
+
+    ``disable()`` makes ``get_tool`` answer None, so the guard reading
+    ``if tool and not allowed`` never fired and the agent was told a
+    gated tool does not exist. Runs in a subprocess because the tier is
+    resolved once at server import.
+    """
+    assert test_id
+
+    code = textwrap.dedent(
+        f"""
+        import asyncio
+        import json
+
+        from fastmcp import Client
+
+        from libtmux_mcp.server import build_mcp_server
+
+        async def main():
+            async with Client(build_mcp_server()) as client:
+                try:
+                    await client.call_tool({tool!r}, {{}})
+                except Exception as exc:
+                    print(json.dumps({{"error": str(exc)}}))
+                else:
+                    print(json.dumps({{"error": None}}))
+
+        asyncio.run(main())
+        """
+    )
+    env = {**os.environ, "LIBTMUX_SAFETY": safety}
+    proc = subprocess.run(
+        [sys.executable, "-c", code],
+        check=True,
+        capture_output=True,
+        env=env,
+        text=True,
+    )
+    error = json.loads(proc.stdout)["error"]
+
+    assert error is not None, f"{tool} should be denied at {safety}"
+    assert "Unknown tool" not in error
+    assert f"requires safety level {required_tier!r}" in error
+    assert f"running at {safety!r}" in error
+    assert f"LIBTMUX_SAFETY={required_tier}" in error
+
+
+def test_batch_distinguishes_gated_tool_from_unknown_tool() -> None:
+    """The batch wrapper must not deny a gated tool's existence either.
+
+    It raised "Unknown tool" on ``get_tool`` returning None, so a gated
+    tool and a misspelled one produced byte-identical rows.
+    """
+    code = textwrap.dedent(
+        """
+        import asyncio
+        import json
+
+        from fastmcp import Client
+
+        from libtmux_mcp.server import build_mcp_server
+
+        async def main():
+            async with Client(build_mcp_server()) as client:
+                out = {}
+                for label, tool in (("gated", "kill_pane"), ("typo", "sned_keys")):
+                    result = await client.call_tool(
+                        "call_mutating_tools_batch",
+                        {"operations": [{"tool": tool, "arguments": {}}]},
+                    )
+                    out[label] = result.structured_content["results"][0]["error"]
+                print(json.dumps(out))
+
+        asyncio.run(main())
+        """
+    )
+    env = {**os.environ, "LIBTMUX_SAFETY": "mutating"}
+    proc = subprocess.run(
+        [sys.executable, "-c", code],
+        check=True,
+        capture_output=True,
+        env=env,
+        text=True,
+    )
+    result = json.loads(proc.stdout)
+
+    assert "requires safety level 'destructive'" in result["gated"]
+    assert "Unknown tool" not in result["gated"]
+    # A genuine misspelling must keep the unknown-tool error, or the fix
+    # would trade one misdescription for another.
+    assert "Unknown tool" in result["typo"]
+
+
+def test_disabled_tools_stay_in_the_registry_with_tags() -> None:
+    """Pin the FastMCP behavior the tier explanation depends on.
+
+    ``_list_tools()`` is private and must keep returning disabled tools
+    with their tags, or the explanation silently reverts.
+    """
+    code = textwrap.dedent(
+        """
+        import asyncio
+        import json
+
+        from libtmux_mcp.server import build_mcp_server
+
+        async def main():
+            mcp = build_mcp_server()
+            registered = {t.name: sorted(t.tags) for t in await mcp._list_tools()}
+            visible = {t.name for t in await mcp.list_tools()}
+            print(json.dumps({
+                "kill_pane_registered": registered.get("kill_pane"),
+                "kill_pane_visible": "kill_pane" in visible,
+                "registered_exceeds_visible": len(registered) > len(visible),
+            }))
+
+        asyncio.run(main())
+        """
+    )
+    env = {**os.environ, "LIBTMUX_SAFETY": "mutating"}
+    proc = subprocess.run(
+        [sys.executable, "-c", code],
+        check=True,
+        capture_output=True,
+        env=env,
+        text=True,
+    )
+    result = json.loads(proc.stdout)
+
+    assert result["kill_pane_registered"] == ["destructive"]
+    assert result["kill_pane_visible"] is False
+    assert result["registered_exceeds_visible"] is True
+
+
 def test_run_server_pins_stdio_transport(monkeypatch: pytest.MonkeyPatch) -> None:
     """run_server passes an explicit stdio transport to FastMCP."""
     from libtmux_mcp import server as server_mod

From aff097a32bb5d6fd6a49fd9268b0f6128899ca0f Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:03:10 -0500
Subject: [PATCH 43/71] Mcp(fix[pane]): Stop reporting tmux failures as success
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Two defects on the same seam: an operation failed and the server told
the agent something that was not true.

`send_keys` dropped any payload starting with `-` and returned
`Keys sent to pane %N`. tmux read the payload as flags and rejected the
command; `Pane.send_keys` builds the argv with no `--` separator and
discards tmux's result, and the wrapper returned a hardcoded success
string. Reachable with ordinary input — `--help` typed into a REPL, a
negative number, a pasted diff line. The argv now ends flag parsing
with `--` and a failed send raises with tmux's own stderr. All three
call sites built argv separately and shared none of it; they now share
one builder, which also fixes the timed batch path (it surfaced the
error but still failed to deliver).

`wait_for_text` crashed with `ValueError: invalid literal for int() with
base 10: ''` when its pane died mid-wait. tmux expands every field of a
vanished pane to the empty string and three `int()` calls on the poll
path took it raw — the same degrade-don't-fail rule the comment three
lines above already states for `alternate_on`.

Fixing only the `int()` calls would have swapped the crash for a wrong
answer: `pane_dead` is itself blanked, so it reads back as `"0"` and
cannot report the death. A killed pane would then have failed the pid
comparison and been reported as *respawned*. A live pane always has a
`pane_pid`, so an empty one is the reliable gone signal, and the wait
now ends with `pane %N died`.

`_read_history_limit` had the same defect one call over — its guard
covered an empty list but not an empty string.

There were no tests for `_parse_pane_state` at all, which is why both
defects survived. The three history tests that asserted on
`Pane.send_keys` calls now observe the argv boundary, which is where
every path already goes.
---
 CHANGES                                   |  33 ++++++
 src/libtmux_mcp/tools/pane_tools/io.py    | 119 ++++++++++++++++++----
 src/libtmux_mcp/tools/pane_tools/state.py |  28 ++++-
 tests/test_history.py                     |  72 ++++++-------
 tests/test_pane_tools.py                  | 104 +++++++++++++++++++
 5 files changed, 287 insertions(+), 69 deletions(-)

diff --git a/CHANGES b/CHANGES
index 7ba3f7b2..b4882bed 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,39 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### A pane that dies mid-wait reports the death, not a parse crash
+
+Killing a pane while `wait_for_text` was waiting on it surfaced
+`Unexpected error: ValueError: invalid literal for int() with base 10:
+''`. tmux expands every field of a vanished pane to the empty string,
+and three `int()` calls on the poll path took it raw.
+
+Fixing only the `int()` calls would have replaced the crash with a wrong
+answer: `pane_dead` is one of the blanked fields, so it reads back as
+`"0"` and cannot report the death itself. A killed pane would then have
+failed the pid comparison instead and been reported as *respawned*. A
+live pane always has a `pane_pid`, so an empty one is the reliable
+signal that the pane is gone, and the wait now ends with `pane %N died;
+cursor/baseline anchor is no longer valid`.
+
+`_read_history_limit` had the same defect one call over — its guard
+covered an empty list but not an empty string — and now shares the
+helper.
+
+#### `send_keys` no longer drops text that starts with `-`
+
+`send_keys(keys="-X cancel", literal=True)` returned `Keys sent to pane
+%N` and sent nothing. tmux parsed the payload as flags and rejected the
+command; the wrapper discarded tmux's result and returned a hardcoded
+success string. Reachable with ordinary input — `--help` or `-v` typed
+into a REPL, a negative number, a pasted diff line.
+
+The `send-keys` argv now ends flag parsing with `--`, and a failed send
+raises with tmux's own stderr instead of reporting success. This covers
+`send_keys` and both `send_keys_batch` paths, which each built the argv
+separately; the timed batch path surfaced the error but still failed to
+deliver.
+
 #### A gated tool now says which tier it needs
 
 Calling a tool above the server's `LIBTMUX_SAFETY` tier reported
diff --git a/src/libtmux_mcp/tools/pane_tools/io.py b/src/libtmux_mcp/tools/pane_tools/io.py
index e534c876..e16cd818 100644
--- a/src/libtmux_mcp/tools/pane_tools/io.py
+++ b/src/libtmux_mcp/tools/pane_tools/io.py
@@ -50,6 +50,89 @@ def _remaining_timeout(deadline: float, timeout: float) -> float:
     return remaining
 
 
+#: Bound on a single untimed ``send-keys``. libtmux runs tmux through
+#: ``Popen.communicate()`` with no timeout, so an unresponsive server
+#: would wedge the tool call. Mirrors ``wait.py``'s per-call ceiling.
+_SEND_KEYS_TIMEOUT_SECONDS = 5.0
+
+
+def _send_keys_argvs(
+    pane: Pane,
+    keys: str,
+    *,
+    enter: bool,
+    literal: bool,
+    suppress_history: bool,
+) -> list[list[str]]:
+    """Build the ``tmux send-keys`` argv(s) for one send.
+
+    ``--`` terminates flag parsing. Without it tmux reads a payload
+    beginning with ``-`` as flags and rejects the command, so `--help`,
+    a negative number, or a pasted diff line never reaches the pane.
+    ``Pane.send_keys`` omits it and discards tmux's result, which is why
+    that failure arrived as a success.
+
+    Enter is a separate call without ``-l`` so it stays a key name
+    rather than the literal text ``Enter``.
+    """
+    pane_id = pane.pane_id
+    if pane_id is None:
+        msg = "resolved pane has no pane_id"
+        raise ExpectedToolError(msg)
+
+    tmux_args = ["send-keys", "-t", pane_id]
+    if literal:
+        tmux_args.append("-l")
+    tmux_args.extend(("--", (" " if suppress_history else "") + keys))
+
+    argvs = [_tmux_argv(pane.server, *tmux_args)]
+    if enter:
+        argvs.append(_tmux_argv(pane.server, "send-keys", "-t", pane_id, "Enter"))
+    return argvs
+
+
+def _raise_send_keys_error(exc: subprocess.CalledProcessError) -> t.NoReturn:
+    """Re-raise a failed ``send-keys`` carrying tmux's own stderr."""
+    stderr = exc.stderr.decode(errors="replace").strip() if exc.stderr else ""
+    msg = f"send-keys failed: {stderr or exc}"
+    raise ExpectedToolError(msg) from exc
+
+
+def _run_send_keys_argv(argv: list[str]) -> None:
+    """Run one ``tmux send-keys`` argv under the untimed ceiling."""
+    try:
+        subprocess.run(
+            argv,
+            check=True,
+            capture_output=True,
+            timeout=_SEND_KEYS_TIMEOUT_SECONDS,
+        )
+    except subprocess.TimeoutExpired as e:
+        msg = f"send-keys timed out after {_SEND_KEYS_TIMEOUT_SECONDS}s"
+        raise ExpectedToolError(msg) from e
+    except subprocess.CalledProcessError as e:
+        _raise_send_keys_error(e)
+
+
+def _run_send_keys(
+    pane: Pane,
+    keys: str,
+    *,
+    enter: bool,
+    literal: bool,
+    suppress_history: bool,
+) -> None:
+    """Send keys to *pane*, raising if tmux rejected them."""
+    for argv in _send_keys_argvs(
+        pane,
+        keys,
+        enter=enter,
+        literal=literal,
+        suppress_history=suppress_history,
+    ):
+        _run_send_keys_argv(argv)
+
+
 def _run_timed_send_keys_argv(
     argv: list[str],
     *,
@@ -67,9 +150,7 @@ def _run_timed_send_keys_argv(
     except subprocess.TimeoutExpired as e:
         raise ExpectedToolError(_batch_timeout_error(timeout)) from e
     except subprocess.CalledProcessError as e:
-        stderr = e.stderr.decode(errors="replace").strip() if e.stderr else ""
-        msg = f"send-keys failed: {stderr or e}"
-        raise ExpectedToolError(msg) from e
+        _raise_send_keys_error(e)
 
 
 def _run_timed_send_keys(
@@ -80,21 +161,13 @@ def _run_timed_send_keys(
     timeout: float,
 ) -> None:
     """Run ``tmux send-keys`` for one operation within the batch deadline."""
-    pane_id = pane.pane_id
-    if pane_id is None:
-        msg = "resolved pane has no pane_id"
-        raise ExpectedToolError(msg)
-
-    tmux_args = ["send-keys", "-t", pane_id]
-    if operation.literal:
-        tmux_args.append("-l")
-    tmux_args.append((" " if operation.suppress_history else "") + operation.keys)
-
-    send_argvs = [_tmux_argv(pane.server, *tmux_args)]
-    if operation.enter:
-        send_argvs.append(_tmux_argv(pane.server, "send-keys", "-t", pane_id, "Enter"))
-
-    for argv in send_argvs:
+    for argv in _send_keys_argvs(
+        pane,
+        operation.keys,
+        enter=operation.enter,
+        literal=operation.literal,
+        suppress_history=operation.suppress_history,
+    ):
         _run_timed_send_keys_argv(argv, deadline=deadline, timeout=timeout)
 
 
@@ -161,11 +234,12 @@ def send_keys(
         session_id=session_id,
         window_id=window_id,
     )
-    pane.send_keys(
+    _run_send_keys(
+        pane,
         keys,
         enter=enter,
-        suppress_history=suppress_history,
         literal=literal,
+        suppress_history=suppress_history,
     )
     return f"Keys sent to pane {pane.pane_id}"
 
@@ -266,11 +340,12 @@ def send_keys_batch(
                     break
                 continue
             if deadline is None:
-                pane.send_keys(
+                _run_send_keys(
+                    pane,
                     operation.keys,
                     enter=operation.enter,
-                    suppress_history=operation.suppress_history,
                     literal=operation.literal,
+                    suppress_history=operation.suppress_history,
                 )
             else:
                 assert timeout is not None
diff --git a/src/libtmux_mcp/tools/pane_tools/state.py b/src/libtmux_mcp/tools/pane_tools/state.py
index d0220aeb..e87c4d02 100644
--- a/src/libtmux_mcp/tools/pane_tools/state.py
+++ b/src/libtmux_mcp/tools/pane_tools/state.py
@@ -62,6 +62,19 @@ class _PaneState(t.NamedTuple):
 HISTORY_LIMIT_FORMAT = "#{history_limit}"
 
 
+def _int_or_zero(value: str) -> int:
+    """Parse a tmux numeric format field, treating a missing value as 0.
+
+    A dead pane makes ``display-message`` expand every field to the
+    empty string, so a bare ``int()`` raised
+    ``ValueError: invalid literal for int() with base 10: ''`` from the
+    hot poll path -- a wait whose pane died reported a raw parse crash
+    rather than ``pane_dead``. Same degrade-don't-fail rule the
+    ``alternate_on`` comment below states.
+    """
+    return int(value) if value else 0
+
+
 def _parse_pane_state(raw: str) -> _PaneState:
     """Parse one :data:`PANE_STATE_FORMAT` line into a :class:`_PaneState`."""
     # ``maxsplit`` is one below the field count so a pane_pid or a
@@ -73,12 +86,17 @@ def _parse_pane_state(raw: str) -> _PaneState:
     parts = raw.split("|", 5)
     hs, cy, sy, pid, dead = parts[:5]
     alternate = parts[5] if len(parts) > 5 else "0"
+    # A pane that no longer exists expands EVERY field to empty --
+    # ``pane_dead`` included, so it reads as "0" and cannot report the
+    # death itself. A live pane always has a pid, so an empty one is
+    # the reliable gone signal; without it the pid mismatch below
+    # reports a killed pane as "respawned".
     return _PaneState(
-        history_size=int(hs),
-        cursor_y=int(cy),
-        pane_height=int(sy),
+        history_size=_int_or_zero(hs),
+        cursor_y=_int_or_zero(cy),
+        pane_height=_int_or_zero(sy),
         pane_pid=pid,
-        pane_dead=dead == "1",
+        pane_dead=dead == "1" or not pid,
         alternate_on=alternate == "1",
     )
 
@@ -130,4 +148,4 @@ def _read_history_limit(pane: Pane) -> int:
     """
     stdout = pane.display_message(HISTORY_LIMIT_FORMAT, get_text=True)
     raw = stdout[0] if stdout else "0"
-    return int(raw)
+    return _int_or_zero(raw)
diff --git a/tests/test_history.py b/tests/test_history.py
index 57ce68ff..fa7b0b62 100644
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -666,14 +666,12 @@ def test_global_history_default_leaves_raw_send_keys_bytes_and_boundaries(
 ) -> None:
     """Control/TUI input stays exact; explicit suppression adds one space.
 
-    A fake pane delegates to libtmux's real ``send_keys`` at the pre-PTY
-    command boundary, where an inherited prefix or merged Enter is observable.
+    Subprocess interception at the pre-PTY argv boundary, where an
+    inherited prefix or a merged Enter would be observable.
     """
-    from libtmux import Pane
-
     from libtmux_mcp.tools.pane_tools import io
 
-    calls: list[tuple[str, tuple[str, ...]]] = []
+    calls: list[list[str]] = []
 
     class FakeServer:
         tmux_bin = "tmux"
@@ -684,18 +682,14 @@ class FakePane:
         pane_id = "%1"
         server = FakeServer()
 
-        def cmd(self, *args: str) -> None:
-            calls.append(("cmd", args))
-
-        def enter(self) -> None:
-            calls.append(("enter", ()))
-
-        def send_keys(self, keys: str, **kwargs: t.Any) -> None:
-            Pane.send_keys(t.cast("Pane", self), keys, **kwargs)
+    def _run(argv: list[str], **kwargs: t.Any) -> subprocess.CompletedProcess[str]:
+        calls.append(argv)
+        return subprocess.CompletedProcess(argv, 0)
 
     pane = FakePane()
     monkeypatch.setattr(io, "_get_server", lambda **kwargs: FakeServer())
     monkeypatch.setattr(io, "_resolve_pane", lambda *args, **kwargs: pane)
+    monkeypatch.setattr("libtmux_mcp.tools.pane_tools.io.subprocess.run", _run)
 
     async def _exercise() -> None:
         async with Client(_history_server("1")) as client:
@@ -721,12 +715,12 @@ async def _exercise() -> None:
     asyncio.run(_exercise())
 
     assert calls == [
-        ("cmd", ("send-keys", "C-c")),
-        ("cmd", ("send-keys", "-l", "partial-TUI")),
-        ("cmd", ("send-keys", "/needle")),
-        ("enter", ()),
-        ("cmd", ("send-keys", "-l", " explicit-secret")),
-        ("enter", ()),
+        ["tmux", "send-keys", "-t", "%1", "--", "C-c"],
+        ["tmux", "send-keys", "-t", "%1", "-l", "--", "partial-TUI"],
+        ["tmux", "send-keys", "-t", "%1", "--", "/needle"],
+        ["tmux", "send-keys", "-t", "%1", "Enter"],
+        ["tmux", "send-keys", "-t", "%1", "-l", "--", " explicit-secret"],
+        ["tmux", "send-keys", "-t", "%1", "Enter"],
     ]
 
 
@@ -735,14 +729,12 @@ def test_global_history_default_leaves_untimed_batch_operations_explicit_only(
 ) -> None:
     """Untimed batches preserve raw defaults, literal mode, and Enter.
 
-    A fake pane exercises libtmux's real ``send_keys`` at the pre-PTY command
-    boundary so exact literal bytes and the separate Enter call remain visible.
+    Subprocess interception at the pre-PTY argv boundary keeps the exact
+    literal bytes and the separate Enter call visible.
     """
-    from libtmux import Pane
-
     from libtmux_mcp.tools.pane_tools import io
 
-    calls: list[tuple[str, tuple[str, ...]]] = []
+    calls: list[list[str]] = []
 
     class FakeServer:
         tmux_bin = "tmux"
@@ -753,18 +745,14 @@ class FakePane:
         pane_id = "%1"
         server = FakeServer()
 
-        def cmd(self, *args: str) -> None:
-            calls.append(("cmd", args))
-
-        def enter(self) -> None:
-            calls.append(("enter", ()))
-
-        def send_keys(self, keys: str, **kwargs: t.Any) -> None:
-            Pane.send_keys(t.cast("Pane", self), keys, **kwargs)
+    def _run(argv: list[str], **kwargs: t.Any) -> subprocess.CompletedProcess[str]:
+        calls.append(argv)
+        return subprocess.CompletedProcess(argv, 0)
 
     pane = FakePane()
     monkeypatch.setattr(io, "_get_server", lambda **kwargs: FakeServer())
     monkeypatch.setattr(io, "_resolve_pane", lambda *args, **kwargs: pane)
+    monkeypatch.setattr("libtmux_mcp.tools.pane_tools.io.subprocess.run", _run)
 
     async def _exercise() -> None:
         async with Client(_history_server("1")) as client:
@@ -793,11 +781,11 @@ async def _exercise() -> None:
     asyncio.run(_exercise())
 
     assert calls == [
-        ("cmd", ("send-keys", "C-c")),
-        ("cmd", ("send-keys", "-l", "TUI_BATCH_DEFAULT")),
-        ("enter", ()),
-        ("cmd", ("send-keys", "-l", " batch-secret")),
-        ("enter", ()),
+        ["tmux", "send-keys", "-t", "%1", "--", "C-c"],
+        ["tmux", "send-keys", "-t", "%1", "-l", "--", "TUI_BATCH_DEFAULT"],
+        ["tmux", "send-keys", "-t", "%1", "Enter"],
+        ["tmux", "send-keys", "-t", "%1", "-l", "--", " batch-secret"],
+        ["tmux", "send-keys", "-t", "%1", "Enter"],
     ]
 
 
@@ -806,8 +794,8 @@ def test_global_history_default_leaves_timed_batch_operations_explicit_only(
 ) -> None:
     """Timed batches preserve raw bytes and send Enter separately.
 
-    Timed batches bypass ``Pane.send_keys``, so subprocess interception at the
-    pre-PTY argv boundary is required to expose prefixes and Enter coalescing.
+    Subprocess interception at the pre-PTY argv boundary exposes prefixes
+    and Enter coalescing.
     """
     from libtmux_mcp.tools.pane_tools import io
 
@@ -859,10 +847,10 @@ async def _exercise() -> None:
     asyncio.run(_exercise())
 
     assert calls == [
-        ["tmux", "send-keys", "-t", "%1", "C-c"],
-        ["tmux", "send-keys", "-t", "%1", "-l", "TUI_BATCH_DEFAULT"],
+        ["tmux", "send-keys", "-t", "%1", "--", "C-c"],
+        ["tmux", "send-keys", "-t", "%1", "-l", "--", "TUI_BATCH_DEFAULT"],
         ["tmux", "send-keys", "-t", "%1", "Enter"],
-        ["tmux", "send-keys", "-t", "%1", "-l", " batch-secret"],
+        ["tmux", "send-keys", "-t", "%1", "-l", "--", " batch-secret"],
         ["tmux", "send-keys", "-t", "%1", "Enter"],
     ]
 
diff --git a/tests/test_pane_tools.py b/tests/test_pane_tools.py
index 1e3d2ff4..4fe234be 100644
--- a/tests/test_pane_tools.py
+++ b/tests/test_pane_tools.py
@@ -227,6 +227,110 @@ def test_send_keys(mcp_server: Server, mcp_pane: Pane) -> None:
     assert "sent" in result.lower()
 
 
+class PaneStateParseFixture(t.NamedTuple):
+    """Test fixture for :func:`_parse_pane_state`."""
+
+    test_id: str
+    raw: str
+    expected_dead: bool
+    expected_height: int
+
+
+PANE_STATE_PARSE_FIXTURES: list[PaneStateParseFixture] = [
+    PaneStateParseFixture("live_pane", "6|0|11|3495270|0|0", False, 11),
+    PaneStateParseFixture("explicitly_dead", "6|0|11|3495270|1|0", True, 11),
+    # tmux blanks every field for a pane that no longer exists.
+    PaneStateParseFixture("pane_gone_all_empty", "|||||", True, 0),
+]
+
+
+@pytest.mark.parametrize(
+    PaneStateParseFixture._fields,
+    PANE_STATE_PARSE_FIXTURES,
+    ids=[fixture.test_id for fixture in PANE_STATE_PARSE_FIXTURES],
+)
+def test_parse_pane_state_survives_a_vanished_pane(
+    test_id: str,
+    raw: str,
+    expected_dead: bool,
+    expected_height: int,
+) -> None:
+    """A vanished pane parses as dead instead of raising.
+
+    Killing a pane mid-wait made ``display-message`` expand every field
+    to empty, and the bare ``int()`` raised ``invalid literal for int()
+    with base 10: ''`` from the poll path. ``pane_dead`` reads empty
+    too, so the empty pid is what identifies the pane as gone.
+    """
+    from libtmux_mcp.tools.pane_tools.state import _parse_pane_state
+
+    assert test_id
+    state = _parse_pane_state(raw)
+
+    assert state.pane_dead is expected_dead
+    assert state.pane_height == expected_height
+
+
+class DashPayloadArgvFixture(t.NamedTuple):
+    """Test fixture for ``--`` placement in the send-keys argv."""
+
+    test_id: str
+    literal: bool
+    enter: bool
+    expected_flags: list[str]
+
+
+DASH_PAYLOAD_ARGV_FIXTURES: list[DashPayloadArgvFixture] = [
+    DashPayloadArgvFixture("literal_no_enter", True, False, ["-l"]),
+    DashPayloadArgvFixture("keyname_with_enter", False, True, []),
+]
+
+
+@pytest.mark.parametrize(
+    DashPayloadArgvFixture._fields,
+    DASH_PAYLOAD_ARGV_FIXTURES,
+    ids=[fixture.test_id for fixture in DASH_PAYLOAD_ARGV_FIXTURES],
+)
+def test_send_keys_argv_terminates_flags_before_the_payload(
+    test_id: str,
+    literal: bool,
+    enter: bool,
+    expected_flags: list[str],
+    mcp_pane: Pane,
+) -> None:
+    """``--`` must sit after the flags and immediately before the text.
+
+    Without it tmux reads a payload beginning with ``-`` as flags and
+    rejects the command, and because ``Pane.send_keys`` discarded the
+    result the tool reported ``Keys sent to pane %N`` for a send that
+    delivered nothing. Asserted on the argv rather than on pane
+    contents: whether un-submitted text echoes into the visible pane
+    depends on the shell and terminal, which varies across CI.
+    """
+    from libtmux_mcp.tools.pane_tools.io import _send_keys_argvs
+
+    assert test_id
+    payload = "-X cancel --help -v"
+    argvs = _send_keys_argvs(
+        mcp_pane,
+        payload,
+        enter=enter,
+        literal=literal,
+        suppress_history=False,
+    )
+
+    send = argvs[0]
+    assert send[-2:] == ["--", payload]
+    for flag in expected_flags:
+        assert flag in send
+    # Enter is a separate call without -l, so it stays a key name
+    # rather than the literal text "Enter".
+    assert len(argvs) == (2 if enter else 1)
+    if enter:
+        assert argvs[1][-1] == "Enter"
+        assert "-l" not in argvs[1]
+
+
 def test_send_keys_batch_sends_operations_in_order(
     mcp_server: Server, mcp_pane: Pane
 ) -> None:

From 0325f1481d590061ab9368f2ccb073e66b934543 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:03:22 -0500
Subject: [PATCH 44/71] Mcp(fix[filters]): Reject unknown filter fields
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

`list_sessions`, `list_windows` and `list_panes` validated the operator
half of a Django-style filter key and never the field half, so
`filters={"nosuch_field__contains": "x"}` returned `[]`. A key with no
`__` at all was not checked by anything — the loop only entered its
branch when one was present, and bound the field to `_field`, the name
that means "deliberately unused".

libtmux's `QueryList` resolves a key by attribute traversal and treats
a miss as "no match", so a misspelled field silently filtered every row
out and the empty result was indistinguishable from a real one. That is
the worst shape in this class: not an error the agent can react to, but
a confident wrong answer shaped like a right one.

Field names are now checked against the object being filtered, with
near-misses suggested, mirroring the operator error that was already
good. Validation covers only the leading segment, so nested traversal
like `active_window__window_name__contains` keeps working — the check
rejects what the type cannot have rather than whitelisting.

The type is a parameter rather than read off the first item, so an
empty list still validates. That is exactly when a typo most needs
reporting.
---
 CHANGES                                | 22 +++++++++++++
 src/libtmux_mcp/_utils.py              | 45 ++++++++++++++++++++++++--
 src/libtmux_mcp/tools/server_tools.py  |  3 +-
 src/libtmux_mcp/tools/session_tools.py |  3 +-
 src/libtmux_mcp/tools/window_tools.py  |  3 +-
 tests/test_utils.py                    | 36 +++++++++++++++++++--
 6 files changed, 104 insertions(+), 8 deletions(-)

diff --git a/CHANGES b/CHANGES
index b4882bed..0bf51074 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,28 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### A typo in a filter field is an error, not an empty list
+
+`list_sessions`, `list_windows` and `list_panes` validated the *operator*
+half of a Django-style filter key and never the *field* half, so
+`filters={"nosuch_field__contains": "x"}` returned `[]`. A key with no
+`__` at all, like `{"totally_bogus": "zzz"}`, was not checked by
+anything. libtmux's `QueryList` resolves a filter key by attribute
+traversal and treats a miss as "no match", so a misspelled field
+silently filtered every row out and the empty result was
+indistinguishable from a genuine one.
+
+Field names are now checked against the object being filtered, with
+near-misses suggested:
+
+```
+Unknown filter field 'session_nme' in 'session_nme__contains'.
+Did you mean: session_name, session_marked, session_id?
+```
+
+Validation covers only the leading segment of a key, so nested
+traversal such as `active_window__window_name__contains` keeps working.
+
 #### A pane that dies mid-wait reports the death, not a parse crash
 
 Killing a pane while `wait_for_text` was waiting on it surfaced
diff --git a/src/libtmux_mcp/_utils.py b/src/libtmux_mcp/_utils.py
index 587e2d85..83f2e6b2 100644
--- a/src/libtmux_mcp/_utils.py
+++ b/src/libtmux_mcp/_utils.py
@@ -7,6 +7,7 @@
 from __future__ import annotations
 
 import dataclasses
+import difflib
 import functools
 import json
 import logging
@@ -841,10 +842,29 @@ def _coerce_dict_arg(
     return value
 
 
+@functools.cache
+def _filterable_fields(obj_type: type) -> frozenset[str]:
+    """Attribute names a filter key may begin with.
+
+    ``QueryList`` resolves a key by ``getattr`` traversal and treats a
+    miss as "no match", so an unknown field silently filters every row
+    out and an empty result is indistinguishable from a typo.
+
+    Deliberately permissive: it rejects names the type cannot have and
+    accepts everything else, because ``__`` traversal into a nested
+    object is legitimate and only the first segment is checkable here.
+    """
+    names = {name for name in dir(obj_type) if not name.startswith("_")}
+    if dataclasses.is_dataclass(obj_type):
+        names |= {field.name for field in dataclasses.fields(obj_type)}
+    return frozenset(names)
+
+
 def _apply_filters(
     items: t.Any,
     filters: dict[str, str] | str | None,
     serializer: t.Callable[..., M],
+    obj_type: type,
 ) -> list[M]:
     """Apply QueryList filters and serialize results.
 
@@ -858,6 +878,11 @@ def _apply_filters(
         If None or empty, all items are returned.
     serializer : callable
         Serializer function to convert each item to a model.
+    obj_type : type
+        libtmux class of the filtered items, used to validate filter
+        field names. Taken as a parameter rather than read off the
+        first item so an empty list still validates -- an empty result
+        is exactly when a typo most needs reporting.
 
     Returns
     -------
@@ -867,7 +892,8 @@ def _apply_filters(
     Raises
     ------
     ExpectedToolError
-        If a filter key uses an invalid lookup operator.
+        If a filter key uses an invalid lookup operator or names a
+        field the object cannot have.
     """
     coerced = _coerce_dict_arg("filters", filters)
     if not coerced:
@@ -875,15 +901,30 @@ def _apply_filters(
     filters = coerced
 
     valid_ops = sorted(LOOKUP_NAME_MAP.keys())
+    allowed_fields = _filterable_fields(obj_type)
     for key in filters:
+        field_path = key
         if "__" in key:
-            _field, op = key.rsplit("__", 1)
+            lhs, op = key.rsplit("__", 1)
             if op not in LOOKUP_NAME_MAP:
                 msg = (
                     f"Invalid filter operator '{op}' in '{key}'. "
                     f"Valid operators: {', '.join(valid_ops)}"
                 )
                 raise ExpectedToolError(msg)
+            field_path = lhs
+
+        # Only the leading segment is checkable; the rest may traverse
+        # into a nested object.
+        field = field_path.split("__", 1)[0]
+        if field not in allowed_fields:
+            msg = f"Unknown filter field '{field}' in '{key}'."
+            close = difflib.get_close_matches(field, sorted(allowed_fields), n=3)
+            if close:
+                msg += f" Did you mean: {', '.join(close)}?"
+            else:
+                msg += " Call this tool without filters to see available fields."
+            raise ExpectedToolError(msg)
 
     filtered = items.filter(**filters)
     return [serializer(item) for item in filtered]
diff --git a/src/libtmux_mcp/tools/server_tools.py b/src/libtmux_mcp/tools/server_tools.py
index d9ab55af..95b454c5 100644
--- a/src/libtmux_mcp/tools/server_tools.py
+++ b/src/libtmux_mcp/tools/server_tools.py
@@ -10,6 +10,7 @@
 import typing as t
 
 from fastmcp.exceptions import ToolError
+from libtmux.session import Session
 
 from libtmux_mcp._history import _prepare_spawn_environment
 from libtmux_mcp._utils import (
@@ -63,7 +64,7 @@ def list_sessions(
     """
     server = _get_server(socket_name=socket_name)
     sessions = server.sessions
-    return _apply_filters(sessions, filters, _serialize_session)
+    return _apply_filters(sessions, filters, _serialize_session, Session)
 
 
 @handle_tool_errors
diff --git a/src/libtmux_mcp/tools/session_tools.py b/src/libtmux_mcp/tools/session_tools.py
index 7805c3ff..ea8d778c 100644
--- a/src/libtmux_mcp/tools/session_tools.py
+++ b/src/libtmux_mcp/tools/session_tools.py
@@ -5,6 +5,7 @@
 import typing as t
 
 from libtmux.constants import WindowDirection
+from libtmux.window import Window
 
 from libtmux_mcp._history import _prepare_spawn_environment
 from libtmux_mcp._utils import (
@@ -72,7 +73,7 @@ def list_windows(
         windows = session.windows
     else:
         windows = server.windows
-    return _apply_filters(windows, filters, _serialize_window)
+    return _apply_filters(windows, filters, _serialize_window, Window)
 
 
 # get_session_info completes the core-tmux-hierarchy symmetry alongside
diff --git a/src/libtmux_mcp/tools/window_tools.py b/src/libtmux_mcp/tools/window_tools.py
index 0a8461b3..a12b2b2e 100644
--- a/src/libtmux_mcp/tools/window_tools.py
+++ b/src/libtmux_mcp/tools/window_tools.py
@@ -5,6 +5,7 @@
 import typing as t
 
 from libtmux.constants import PaneDirection
+from libtmux.pane import Pane
 
 from libtmux_mcp._history import _prepare_spawn_environment
 from libtmux_mcp._utils import (
@@ -98,7 +99,7 @@ def list_panes(
         panes = session.panes
     else:
         panes = server.panes
-    return _apply_filters(panes, filters, _serialize_pane)
+    return _apply_filters(panes, filters, _serialize_pane, Pane)
 
 
 # get_window_info completes the core-tmux-hierarchy symmetry of get_*_info
diff --git a/tests/test_utils.py b/tests/test_utils.py
index d5ee8f3d..578b924a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -8,6 +8,7 @@
 import pytest
 from fastmcp.exceptions import ToolError
 from libtmux import exc
+from libtmux.session import Session
 
 from libtmux_mcp._utils import (
     ANNOTATIONS_CREATE,
@@ -34,7 +35,6 @@
 if t.TYPE_CHECKING:
     from libtmux.pane import Pane
     from libtmux.server import Server
-    from libtmux.session import Session
     from libtmux.window import Window
 
 
@@ -212,6 +212,36 @@ class ApplyFiltersFixture(t.NamedTuple):
         expect_error=True,
         error_match="Invalid filter operator",
     ),
+    # A typo'd FIELD used to return [] rather than erroring, so an empty
+    # result was indistinguishable from "nothing matched".
+    ApplyFiltersFixture(
+        test_id="unknown_field_with_valid_operator_errors",
+        filters={"nosuch_field__contains": "x"},
+        expected_count=None,
+        expect_error=True,
+        error_match="Unknown filter field 'nosuch_field'",
+    ),
+    ApplyFiltersFixture(
+        test_id="unknown_field_without_operator_errors",
+        filters={"totally_bogus": "zzz"},
+        expected_count=None,
+        expect_error=True,
+        error_match="Unknown filter field 'totally_bogus'",
+    ),
+    ApplyFiltersFixture(
+        test_id="near_miss_field_suggests_alternatives",
+        filters={"session_nme__contains": "x"},
+        expected_count=None,
+        expect_error=True,
+        error_match="Did you mean: session_name",
+    ),
+    ApplyFiltersFixture(
+        test_id="nested_traversal_still_allowed",
+        filters={"active_window__window_name__contains": ""},
+        expected_count=None,
+        expect_error=False,
+        error_match=None,
+    ),
     ApplyFiltersFixture(
         test_id="contains_operator",
         filters={"session_name__contains": ""},
@@ -295,9 +325,9 @@ def test_apply_filters(
 
     if expect_error:
         with pytest.raises(ToolError, match=error_match):
-            _apply_filters(sessions, filters, _serialize_session)
+            _apply_filters(sessions, filters, _serialize_session, Session)
     else:
-        result = _apply_filters(sessions, filters, _serialize_session)
+        result = _apply_filters(sessions, filters, _serialize_session, Session)
         assert isinstance(result, list)
         if expected_count is not None:
             assert len(result) == expected_count

From e312b46434899ffeab37e731d2494e995db483a6 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:16:16 -0500
Subject: [PATCH 45/71] Mcp(fix[errors]): Diagnose a newline in a path; stop
 doubling a prefix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A pane whose current directory contains a newline makes libtmux fail to
parse `-F` output, and because every pane lookup enumerates panes, the
whole server stops resolving — healthy panes included. It reached the
agent as `Unexpected error: ValueError: zip() argument 2 is shorter than
argument 1`, logged at ERROR, naming nothing it could act on. The agent
could not repair it through the MCP either: every tool that could have
moved the pane out needed the same enumeration.

It is now an expected failure that names the cause, says the blast
radius is server-wide rather than one pane, and gives the command that
locates the offender. Matched on the message because the raise site is
a stdlib `zip` with no dedicated exception type.

The parse itself is fixed upstream in tmux-python/libtmux#752, but this
diagnosis is kept rather than deferred: the floor is `libtmux>=0.62.0`
and the installed version is not this package's to choose.

`exc.PaneNotFound` prefixes its own message and the mapper prefixed it
again, so the most frequently hit error in the server read `Pane not
found: Pane not found: %9999`.
---
 CHANGES                   | 22 ++++++++++++++++++++++
 src/libtmux_mcp/_utils.py | 35 ++++++++++++++++++++++++++++++++++-
 tests/test_utils.py       | 37 +++++++++++++++++++++++++++++++++++++
 3 files changed, 93 insertions(+), 1 deletion(-)

diff --git a/CHANGES b/CHANGES
index 0bf51074..230f6420 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,28 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### A newline in a directory name is diagnosed, not "Unexpected error"
+
+A pane whose current directory contains a newline makes libtmux fail to
+parse `-F` output, and because every pane lookup enumerates panes, the
+whole tmux server stops resolving — healthy panes included. It reached
+the agent as `Unexpected error: ValueError: zip() argument 2 is shorter
+than argument 1`, logged at ERROR, naming nothing it could act on. The
+agent also could not repair it through the MCP, since every tool that
+could move the pane needed the same enumeration.
+
+It is now an expected failure that names the cause and how to find the
+offending pane. The parse itself is fixed in libtmux
+([#752](https://github.com/tmux-python/libtmux/pull/752)); this
+diagnosis stays useful regardless, because the installed libtmux
+version is not this package's to choose.
+
+#### `Pane not found:` is no longer said twice
+
+`exc.PaneNotFound` prefixes its own message and the error mapper
+prefixed it again, so the most frequently hit error in the server read
+`Pane not found: Pane not found: %9999`.
+
 #### A typo in a filter field is an error, not an empty list
 
 `list_sessions`, `list_windows` and `list_panes` validated the *operator*
diff --git a/src/libtmux_mcp/_utils.py b/src/libtmux_mcp/_utils.py
index 83f2e6b2..570790fd 100644
--- a/src/libtmux_mcp/_utils.py
+++ b/src/libtmux_mcp/_utils.py
@@ -1070,6 +1070,27 @@ def _serialize_pane(pane: Pane) -> PaneInfo:
 R = t.TypeVar("R")
 
 
+def _undouble(prefix: str, text: str) -> str:
+    """Drop *prefix* from *text* when the wrapper is about to add it back."""
+    return text.removeprefix(prefix)
+
+
+def _is_format_newline_parse_error(e: BaseException) -> bool:
+    """Detect libtmux failing to parse a format value containing a newline.
+
+    libtmux <= 0.62.0 splits ``-F`` output one line per object, so a
+    newline inside any value (a pane's current directory, most reachably)
+    splits that record and its strict ``zip`` raises. It surfaces as a
+    bare ``ValueError`` and would otherwise reach the agent as
+    "Unexpected error", logged at ERROR, naming nothing it can act on.
+
+    Matched on the message because the raise site is a stdlib ``zip``
+    with no dedicated exception type. Kept even once the floor moves
+    past the libtmux fix: the installed version is not ours to choose.
+    """
+    return isinstance(e, ValueError) and "zip()" in str(e)
+
+
 def _map_exception_to_tool_error(fn_name: str, e: BaseException) -> ToolError:
     """Translate a libtmux / unexpected exception into a ``ToolError``.
 
@@ -1107,9 +1128,21 @@ def _map_exception_to_tool_error(fn_name: str, e: BaseException) -> ToolError:
         )
     if isinstance(e, exc.PaneNotFound):
         return ExpectedToolError(
-            f"Pane not found: {e}",
+            f"Pane not found: {_undouble('Pane not found: ', str(e))}",
             suggestion="Call list_panes to discover valid pane ids.",
         )
+    if _is_format_newline_parse_error(e):
+        return ExpectedToolError(
+            "tmux listing could not be parsed: a format value contains a "
+            "newline, almost always a pane whose current directory has one "
+            "in its name. Every pane on this server is affected, not just "
+            "that one, because pane lookup enumerates them all.",
+            suggestion=(
+                "Find it with: tmux list-panes -a -F "
+                "'#{pane_id} #{pane_current_path}' | cat -A — then move or "
+                "rename that directory. Upgrading libtmux also fixes it."
+            ),
+        )
     if isinstance(e, exc.LibTmuxException):
         return ExpectedToolError(f"tmux error: {e}")
     logger.exception("unexpected error in MCP tool %s", fn_name)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 578b924a..dda354ba 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -944,6 +944,43 @@ def test_map_exception_operator_faults_stay_at_error(raised: Exception) -> None:
     assert mapped.log_level == logging.ERROR
 
 
+def test_map_exception_explains_a_newline_in_a_format_value() -> None:
+    """The newline-in-a-path parse failure becomes actionable.
+
+    libtmux <= 0.62.0 splits ``-F`` output one line per object, so a
+    newline inside a value breaks its strict ``zip`` and every pane on
+    that server stops resolving. It arrives as a bare ``ValueError`` and
+    previously reached the agent as "Unexpected error", at ERROR,
+    naming nothing it could act on.
+    """
+    from libtmux_mcp._utils import ExpectedToolError, _map_exception_to_tool_error
+
+    raised = ValueError("zip() argument 2 is shorter than argument 1")
+    mapped = _map_exception_to_tool_error("list_panes", raised)
+
+    assert isinstance(mapped, ExpectedToolError)
+    assert "newline" in str(mapped)
+    assert mapped.suggestion is not None
+    assert "pane_current_path" in mapped.suggestion
+
+
+def test_map_exception_does_not_double_the_pane_prefix() -> None:
+    """``Pane not found: Pane not found: %9`` said it twice.
+
+    ``exc.PaneNotFound`` already prefixes its own message, and the
+    mapper prefixed it again — visible on the most frequently hit error
+    in the server.
+    """
+    from libtmux_mcp._utils import _map_exception_to_tool_error
+
+    raised = exc.PaneNotFound("%9999")
+    assert str(raised) == "Pane not found: %9999"
+
+    mapped = _map_exception_to_tool_error("get_pane_info", raised)
+
+    assert str(mapped) == "Pane not found: %9999"
+
+
 def test_expected_tool_error_logs_warning_through_server(
     caplog: pytest.LogCaptureFixture,
 ) -> None:

From d6d78737c167433f4d8583d1f8f452e563ac6464 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:26:46 -0500
Subject: [PATCH 46/71] Mcp(fix[server]): Tell an unreachable server from an
 absent one
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

When the tmux binary running this server is older than the one that
created a socket, `get_server_info` returned `is_alive=False,
session_count=0` and `list_sessions` returned `[]`, both without error.
An agent reads that as "the user's work is gone". Two of four calls
answered with a confident falsehood; `list_panes` did error.

`Server.is_alive()` answers `False` both for a socket with no daemon and
for a live server this binary cannot speak to, and `Server.sessions`
degrades to `[]` in both cases. libtmux's own `sessions` docstring
points at `is_alive` to tell the two apart, but it cannot — they
collapse to the same `False`. tmux distinguishes them on stderr, so
`_probe_liveness` reads that instead of the boolean.

`list_sessions` probes only when the listing came back empty: a server
that listed anything cannot be unreachable, so the common path keeps
its single round trip on the most-called discovery tool.

`ServerInfo` gains `unreachable_reason`. When set, `is_alive=False`
means "could not ask", not "not running", and `session_count=0` carries
no information. `list_servers`' socket scan shares the probe.

The trigger is an ordinary tmux upgrade — sockets outlive the binary
that made them. Verified against a real 3.2a client and a 3.7c server.

Also: `exit_copy_mode` on a pane that was not in a mode returned a full
`PaneInfo`, reading as confirmation the pane had left copy mode, while
tmux said `not in a mode` and exited 1.
`Pane.send_keys(copy_mode_cmd=...)` discards that result the same way
the ordinary send path did. Both copy-mode call sites now share a
helper that raises with tmux's stderr; no `--` is needed there because
every command is a module constant rather than caller text.

The liveness tests drive a crafted result rather than a second tmux
binary, so they assert the discrimination without depending on which
tmux a CI job installed.
---
 CHANGES                                       | 33 +++++++++
 src/libtmux_mcp/_utils.py                     | 42 ++++++++++++
 src/libtmux_mcp/models.py                     |  9 +++
 src/libtmux_mcp/tools/pane_tools/copy_mode.py | 37 ++++++++--
 src/libtmux_mcp/tools/server_tools.py         | 29 +++++++-
 tests/test_pane_tools.py                      | 20 ++++++
 tests/test_utils.py                           | 67 +++++++++++++++++++
 7 files changed, 227 insertions(+), 10 deletions(-)

diff --git a/CHANGES b/CHANGES
index 230f6420..ca233499 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,39 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### A live server no longer reports as absent
+
+When the tmux binary running this server is older than the one that
+created a socket, `get_server_info` returned `is_alive=False,
+session_count=0` and `list_sessions` returned `[]` — both without error.
+An agent reads that as "the user's work is gone". `list_panes` did error,
+so two of four calls answered with a confident falsehood.
+
+`Server.is_alive()` answers `False` both for a socket with no daemon and
+for a live server this binary cannot speak to, and `Server.sessions`
+degrades to `[]` in both cases. libtmux's own docstring points at
+`is_alive` to tell the two apart, but it cannot — they collapse to the
+same `False`. tmux itself distinguishes them on stderr, so that is read
+instead of the boolean.
+
+`list_sessions` now raises rather than claiming a server it could not
+query is empty, and names the likely cause. `ServerInfo` gains
+`unreachable_reason`: when it is set, `is_alive=False` means "could not
+ask", not "not running", and `session_count=0` carries no information.
+
+The trigger is an ordinary tmux upgrade — sockets outlive the binary
+that made them. The boundary is between tmux 3.5 and 3.6, and only in
+the old-client-to-new-server direction; a newer binary reads an older
+server correctly.
+
+#### A rejected copy-mode command is no longer reported as success
+
+`exit_copy_mode` on a pane that was not in a mode returned a full
+`PaneInfo`, which reads as confirmation the pane left copy mode. tmux
+says `not in a mode` and exits 1. `Pane.send_keys(copy_mode_cmd=...)`
+discards that result, the same way the ordinary send path did.
+`enter_copy_mode(scroll_up=...)` used the same unchecked call.
+
 #### A newline in a directory name is diagnosed, not "Unexpected error"
 
 A pane whose current directory contains a newline makes libtmux fail to
diff --git a/src/libtmux_mcp/_utils.py b/src/libtmux_mcp/_utils.py
index 570790fd..5c04e6e7 100644
--- a/src/libtmux_mcp/_utils.py
+++ b/src/libtmux_mcp/_utils.py
@@ -1070,6 +1070,48 @@ def _serialize_pane(pane: Pane) -> PaneInfo:
 R = t.TypeVar("R")
 
 
+#: tmux stderr fragments that mean the socket genuinely has no daemon
+#: behind it. Anything else on a failed ``list-sessions`` -- a protocol
+#: mismatch, a permission error -- means a server that exists and cannot
+#: be talked to, which is a different answer.
+_NO_SERVER_MARKERS = (
+    "no server running",
+    "no such file or directory",
+    "error connecting to",
+)
+
+
+def _probe_liveness(server: Server) -> tuple[bool, str | None]:
+    """Return ``(alive, unreachable_reason)`` for *server*.
+
+    ``Server.is_alive()`` answers False for a socket with no daemon AND
+    for a live server this tmux binary cannot speak to, and
+    ``Server.sessions`` degrades to ``[]`` in both cases. libtmux's own
+    docstring points at ``is_alive`` to tell those apart, but it cannot:
+    both collapse to the same False.
+
+    The difference matters because they warrant opposite reactions. "No
+    server" is a fact an agent can act on; "cannot reach the server" over
+    a socket whose daemon is running -- an ordinary tmux upgrade leaves
+    sockets older than the binary -- reported as False tells the agent
+    the user's work is gone. tmux distinguishes them on stderr, so read
+    it rather than the boolean.
+    """
+    try:
+        result = server.cmd("list-sessions")
+    except Exception as err:  # noqa: BLE001 - probe must not raise
+        return False, str(err)
+
+    if result.returncode == 0:
+        return True, None
+
+    detail = " ".join(result.stderr).strip() if result.stderr else ""
+    lowered = detail.lower()
+    if any(marker in lowered for marker in _NO_SERVER_MARKERS):
+        return False, None
+    return False, detail or f"tmux exited with status {result.returncode}"
+
+
 def _undouble(prefix: str, text: str) -> str:
     """Drop *prefix* from *text* when the wrapper is about to add it back."""
     return text.removeprefix(prefix)
diff --git a/src/libtmux_mcp/models.py b/src/libtmux_mcp/models.py
index 90733671..538fa2af 100644
--- a/src/libtmux_mcp/models.py
+++ b/src/libtmux_mcp/models.py
@@ -200,6 +200,15 @@ class ServerInfo(BaseModel):
     socket_path: str | None = Field(default=None, description="Socket path")
     session_count: int = Field(description="Number of sessions")
     version: str | None = Field(default=None, description="tmux version")
+    unreachable_reason: str | None = Field(
+        default=None,
+        description=(
+            "Why a server that exists could not be queried, e.g. this tmux "
+            "binary being older than the one that created the socket. When "
+            "set, is_alive=False means 'could not ask', NOT 'not running', "
+            "and session_count=0 carries no information."
+        ),
+    )
 
 
 class OptionResult(BaseModel):
diff --git a/src/libtmux_mcp/tools/pane_tools/copy_mode.py b/src/libtmux_mcp/tools/pane_tools/copy_mode.py
index 6c9627aa..754b2c5c 100644
--- a/src/libtmux_mcp/tools/pane_tools/copy_mode.py
+++ b/src/libtmux_mcp/tools/pane_tools/copy_mode.py
@@ -2,7 +2,10 @@
 
 from __future__ import annotations
 
+import typing as t
+
 from libtmux_mcp._utils import (
+    ExpectedToolError,
     _get_server,
     _resolve_pane,
     _serialize_pane,
@@ -12,6 +15,31 @@
     PaneInfo,
 )
 
+if t.TYPE_CHECKING:
+    from libtmux.pane import Pane
+
+
+def _run_copy_mode_cmd(pane: Pane, command: str, *, repeat: int | None = None) -> None:
+    """Send one ``-X`` copy-mode command, raising if tmux rejected it.
+
+    ``Pane.send_keys(copy_mode_cmd=...)`` discards tmux's result, so
+    cancelling a pane that is not in a mode came back as a completed
+    operation and the returned ``PaneInfo`` read like confirmation the
+    pane had left copy mode. tmux says ``not in a mode`` and exits 1.
+
+    No ``--`` here, unlike the ordinary send path: every *command* is a
+    module constant, never caller text.
+    """
+    args = ["send-keys"]
+    if repeat is not None:
+        args.extend(("-N", str(repeat)))
+    args.extend(("-X", command))
+    result = pane.cmd(*args)
+    if result.returncode != 0 or result.stderr:
+        detail = " ".join(result.stderr).strip() if result.stderr else ""
+        msg = f"copy-mode command {command!r} failed: {detail or 'tmux exited 1'}"
+        raise ExpectedToolError(msg)
+
 
 @handle_tool_errors
 def enter_copy_mode(
@@ -57,12 +85,7 @@ def enter_copy_mode(
     )
     pane.copy_mode()
     if scroll_up is not None and scroll_up > 0:
-        pane.send_keys(
-            "",
-            copy_mode_cmd="scroll-up",
-            repeat=scroll_up,
-            enter=False,
-        )
+        _run_copy_mode_cmd(pane, "scroll-up", repeat=scroll_up)
     pane.refresh()
     return _serialize_pane(pane)
 
@@ -106,6 +129,6 @@ def exit_copy_mode(
         session_id=session_id,
         window_id=window_id,
     )
-    pane.send_keys("", copy_mode_cmd="cancel", enter=False)
+    _run_copy_mode_cmd(pane, "cancel")
     pane.refresh()
     return _serialize_pane(pane)
diff --git a/src/libtmux_mcp/tools/server_tools.py b/src/libtmux_mcp/tools/server_tools.py
index 95b454c5..8075101f 100644
--- a/src/libtmux_mcp/tools/server_tools.py
+++ b/src/libtmux_mcp/tools/server_tools.py
@@ -26,6 +26,7 @@
     _get_caller_identity,
     _get_server,
     _invalidate_server,
+    _probe_liveness,
     _serialize_session,
     handle_tool_errors,
 )
@@ -64,6 +65,26 @@ def list_sessions(
     """
     server = _get_server(socket_name=socket_name)
     sessions = server.sessions
+    # ``Server.sessions`` degrades to [] for a server it could not reach
+    # as well as for one with no sessions, so an empty answer is the only
+    # ambiguous one. Probe only then: a server that listed anything
+    # cannot be unreachable, and this is the most-called discovery tool.
+    if not sessions:
+        _, unreachable = _probe_liveness(server)
+        if unreachable is not None:
+            msg = (
+                f"tmux server exists but could not be queried: {unreachable}. "
+                "Reporting no sessions would claim it is empty."
+            )
+            raise ExpectedToolError(
+                msg,
+                suggestion=(
+                    "Most often this tmux binary is older than the one that "
+                    "started the server; sockets outlive the binary that made "
+                    "them. Compare `tmux -V` with the server's own version and "
+                    "set LIBTMUX_TMUX_BIN to the matching binary."
+                ),
+            )
     return _apply_filters(sessions, filters, _serialize_session, Session)
 
 
@@ -194,7 +215,7 @@ def get_server_info(socket_name: str | None = None) -> ServerInfo:
         Server information.
     """
     server = _get_server(socket_name=socket_name)
-    alive = server.is_alive()
+    alive, unreachable = _probe_liveness(server)
     version: str | None = None
     try:
         result = server.cmd("display-message", "-p", "#{version}")
@@ -212,6 +233,7 @@ def get_server_info(socket_name: str | None = None) -> ServerInfo:
         socket_path=str(server.socket_path) if server.socket_path else None,
         session_count=len(server.sessions) if alive else 0,
         version=version,
+        unreachable_reason=unreachable,
     )
 
 
@@ -261,9 +283,9 @@ def _probe_server_by_path(socket_path: pathlib.Path) -> ServerInfo | None:
         return None
     server = _get_server(socket_path=str(socket_path))
     try:
-        alive = server.is_alive()
+        alive, unreachable = _probe_liveness(server)
     except Exception as err:
-        logger.debug("probe %s: is_alive raised %s", socket_path, err)
+        logger.debug("probe %s: liveness probe raised %s", socket_path, err)
         return None
     version: str | None = None
     try:
@@ -277,6 +299,7 @@ def _probe_server_by_path(socket_path: pathlib.Path) -> ServerInfo | None:
         socket_path=str(socket_path),
         session_count=len(server.sessions) if alive else 0,
         version=version,
+        unreachable_reason=unreachable,
     )
 
 
diff --git a/tests/test_pane_tools.py b/tests/test_pane_tools.py
index 4fe234be..6e165f2e 100644
--- a/tests/test_pane_tools.py
+++ b/tests/test_pane_tools.py
@@ -271,6 +271,26 @@ def test_parse_pane_state_survives_a_vanished_pane(
     assert state.pane_height == expected_height
 
 
+def test_exit_copy_mode_reports_a_pane_that_is_not_in_a_mode(
+    mcp_server: Server, mcp_pane: Pane
+) -> None:
+    """A copy-mode command tmux rejected must not read as success.
+
+    ``Pane.send_keys(copy_mode_cmd=...)`` discards tmux's result, so
+    cancelling a pane that is not in a mode returned a full ``PaneInfo``
+    that looked like confirmation the pane had left copy mode. tmux says
+    ``not in a mode`` and exits 1.
+    """
+    from libtmux_mcp.tools.pane_tools.copy_mode import enter_copy_mode, exit_copy_mode
+
+    with pytest.raises(ToolError, match="not in a mode"):
+        exit_copy_mode(pane_id=mcp_pane.pane_id, socket_name=mcp_server.socket_name)
+
+    # Control: the real flow still works, so the guard is not blanket.
+    enter_copy_mode(pane_id=mcp_pane.pane_id, socket_name=mcp_server.socket_name)
+    exit_copy_mode(pane_id=mcp_pane.pane_id, socket_name=mcp_server.socket_name)
+
+
 class DashPayloadArgvFixture(t.NamedTuple):
     """Test fixture for ``--`` placement in the send-keys argv."""
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
index dda354ba..c0f19c45 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -944,6 +944,73 @@ def test_map_exception_operator_faults_stay_at_error(raised: Exception) -> None:
     assert mapped.log_level == logging.ERROR
 
 
+class LivenessProbeFixture(t.NamedTuple):
+    """Test fixture for :func:`_probe_liveness`."""
+
+    test_id: str
+    returncode: int
+    stderr: list[str]
+    expected_alive: bool
+    expected_unreachable: str | None
+
+
+LIVENESS_PROBE_FIXTURES: list[LivenessProbeFixture] = [
+    LivenessProbeFixture("running", 0, [], True, None),
+    LivenessProbeFixture(
+        "no_daemon", 1, ["no server running on /tmp/tmux-1000/x"], False, None
+    ),
+    LivenessProbeFixture("missing_socket", 1, ["error connecting to /x"], False, None),
+    # A live server this tmux binary cannot speak to. Reporting it the
+    # same as "no server" tells the agent the user's work is gone.
+    LivenessProbeFixture(
+        "protocol_mismatch",
+        1,
+        ["server exited unexpectedly"],
+        False,
+        "server exited unexpectedly",
+    ),
+]
+
+
+@pytest.mark.parametrize(
+    LivenessProbeFixture._fields,
+    LIVENESS_PROBE_FIXTURES,
+    ids=[fixture.test_id for fixture in LIVENESS_PROBE_FIXTURES],
+)
+def test_probe_liveness_separates_absent_from_unreachable(
+    test_id: str,
+    returncode: int,
+    stderr: list[str],
+    expected_alive: bool,
+    expected_unreachable: str | None,
+) -> None:
+    """Absent and unreachable are different answers.
+
+    ``Server.is_alive()`` collapses both to False and ``Server.sessions``
+    degrades to ``[]`` for both, so an ordinary tmux upgrade -- sockets
+    outlive the binary that made them -- made a live server report as
+    absent with no error. Driven off a fake result rather than a second
+    tmux binary so the assertion does not depend on the CI tmux version.
+    """
+    from libtmux_mcp._utils import _probe_liveness
+
+    assert test_id
+
+    class _Result:
+        def __init__(self) -> None:
+            self.returncode = returncode
+            self.stderr = stderr
+
+    class _Server:
+        def cmd(self, *args: str) -> _Result:
+            return _Result()
+
+    alive, unreachable = _probe_liveness(t.cast("t.Any", _Server()))
+
+    assert alive is expected_alive
+    assert unreachable == expected_unreachable
+
+
 def test_map_exception_explains_a_newline_in_a_format_value() -> None:
     """The newline-in-a-path parse failure becomes actionable.
 

From b6c105ef918fb40f4a31622c012ce1863d1ceca8 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:36:28 -0500
Subject: [PATCH 47/71] Mcp(fix[wait]): Report a stop marker that was already
 on screen
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The entry scan covered `patterns` and not `stop`, so waiting on a pane
that already showed a failure marker returned a bare `timeout` with no
sign the marker had been there the whole time. The realistic shape: an
agent runs a build, it fails, the agent waits for the next build without
clearing, and reads `timeout` as "still running" when the honest answer
is "the previous run already failed".

`WaitForTextResult` gains `stop_matched_at_entry`, kept separate from
`matched_at_entry` because a stale success marker and a stale failure
marker call for opposite reactions. A stale stop hit still does not end
the wait — only a fresh one does — so this is a diagnostic, not a
behavior change.

The rationale is the one already written above `matched_at_entry`'s own
entry scan: an agent must be able to tell "already there" from "never
arrived". That reasoning was applied to the success patterns and not to
the stop patterns three lines away.

Also corrects the server instructions. `stop=[] bails` was parsed by two
independent readers as "the empty list bails". `stop=[]` is accepted and
behaves like `stop=null`; it is a stop *hit* that returns immediately.
---
 CHANGES                                  | 19 +++++++++++++
 src/libtmux_mcp/models.py                | 11 ++++++++
 src/libtmux_mcp/server.py                |  2 +-
 src/libtmux_mcp/tools/pane_tools/wait.py |  8 ++++++
 tests/test_pane_tools.py                 | 36 ++++++++++++++++++++++++
 tests/test_server.py                     |  4 ++-
 6 files changed, 78 insertions(+), 2 deletions(-)

diff --git a/CHANGES b/CHANGES
index ca233499..8299ed2e 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,25 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### `wait_for_text` reports a stop marker that was already on screen
+
+The entry scan covered `patterns` and not `stop`, so waiting on a pane
+that already showed a failure marker returned a bare `timeout` with no
+sign the marker had been there the whole time. The realistic shape: an
+agent runs a build, it fails, the agent waits for the next build without
+clearing, and reads `timeout` as "still running" when the honest answer
+is "the previous run already failed".
+
+`WaitForTextResult` gains `stop_matched_at_entry`, kept separate from
+`matched_at_entry` because a stale success marker and a stale failure
+marker call for opposite reactions. A stale stop hit still does not end
+the wait — only a fresh one does.
+
+The server instructions described this as `stop=[] bails`, which two
+independent readers parsed as "the empty list bails". `stop=[]` is
+accepted and behaves like `stop=null`; it is a stop *hit* that returns
+immediately, and the text now says so.
+
 #### A live server no longer reports as absent
 
 When the tmux binary running this server is older than the one that
diff --git a/src/libtmux_mcp/models.py b/src/libtmux_mcp/models.py
index 538fa2af..3bcb38b1 100644
--- a/src/libtmux_mcp/models.py
+++ b/src/libtmux_mcp/models.py
@@ -308,6 +308,17 @@ class WaitForTextResult(BaseModel):
             "``alternate_screen``."
         ),
     )
+    stop_matched_at_entry: bool = Field(
+        default=False,
+        description=(
+            "True when a ``stop`` pattern was already on screen before the "
+            "wait began. The wait only stops on a FRESH stop hit, so this "
+            "does not end it -- but a failure marker left from an earlier "
+            "run is the usual reason a ``timeout`` outcome is misread as "
+            "'still running'. Read it as: check whether you are waiting on "
+            "a run that already failed."
+        ),
+    )
     matched_at_entry: bool = Field(
         default=False,
         description=(
diff --git a/src/libtmux_mcp/server.py b/src/libtmux_mcp/server.py
index d7934586..a38407c3 100644
--- a/src/libtmux_mcp/server.py
+++ b/src/libtmux_mcp/server.py
@@ -108,7 +108,7 @@
     "WAIT, DON'T POLL: run_command for authored commands needing "
     "status; wait_for_channel for custom tmux wait-for; capture_since "
     "for tailing; wait_for_text for output you don't author "
-    "(patterns=null=any output; stop=[] bails); "
+    "(patterns=null=any output; a stop hit returns at once); "
     "send_keys_batch for raw input."
 )
 
diff --git a/src/libtmux_mcp/tools/pane_tools/wait.py b/src/libtmux_mcp/tools/pane_tools/wait.py
index 9f58df84..60c8a9d2 100644
--- a/src/libtmux_mcp/tools/pane_tools/wait.py
+++ b/src/libtmux_mcp/tools/pane_tools/wait.py
@@ -653,6 +653,13 @@ async def wait_for_text(
     # single most common reason a wait "should have" matched instantly
     # and instead ran to the ceiling.
     stale_at_entry = _first_match(compiled_patterns, visible_rows) is not None
+    # Same rationale applied to ``stop``. A failure marker already on
+    # screen is the case where a bare "timeout" misleads most: an agent
+    # re-running a build reads it as "still running" when the honest
+    # answer is "the previous run already failed". Kept a separate field
+    # because "my success text predates the call" and "my failure text
+    # predates the call" call for opposite reactions.
+    stop_stale_at_entry = _first_match(compiled_stop, visible_rows) is not None
 
     matched_lines: list[str] = []
     outcome: _WaitOutcome = "timeout"
@@ -854,6 +861,7 @@ async def wait_for_text(
         matched_lines=limited_matches.lines,
         saw_new_output=saw_new_output,
         matched_at_entry=stale_at_entry and not found,
+        stop_matched_at_entry=stop_stale_at_entry,
         alternate_screen=saw_alternate_screen,
         tail=limited_tail.lines,
         pane_id=target,
diff --git a/tests/test_pane_tools.py b/tests/test_pane_tools.py
index 6e165f2e..ff3e6a1d 100644
--- a/tests/test_pane_tools.py
+++ b/tests/test_pane_tools.py
@@ -3491,6 +3491,42 @@ def _stale_marker_visible() -> bool:
     assert result.matched_at_entry is False
 
 
+def test_wait_for_text_reports_a_stop_marker_already_on_screen(
+    mcp_server: Server, mcp_pane: Pane
+) -> None:
+    """A failure marker predating the wait is surfaced, not hidden.
+
+    The entry scan covered ``patterns`` and not ``stop``, so a build
+    that had already failed produced a bare ``timeout`` — which an agent
+    re-running the build reads as "still running" when the honest answer
+    is "the previous run already failed".
+    """
+    import asyncio
+
+    marker = "STOP_ALREADY_THERE"
+    _park_pane(mcp_pane)
+    _write_to_pane_tty(mcp_pane, f"\n{marker}\n")
+    retry_until(
+        lambda: any(marker in line for line in mcp_pane.capture_pane()),
+        5,
+        raises=True,
+    )
+
+    result = asyncio.run(
+        wait_for_text(
+            patterns=["NEVER_APPEARS_ZZZ"],
+            stop=[marker],
+            pane_id=mcp_pane.pane_id,
+            timeout=2.0,
+            socket_name=mcp_server.socket_name,
+        )
+    )
+
+    # The stale stop marker must not END the wait -- only a fresh hit does.
+    assert result.outcome == "timeout"
+    assert result.stop_matched_at_entry is True
+
+
 def test_wait_for_text_ignores_stale_below_cursor(
     mcp_server: Server, mcp_pane: Pane
 ) -> None:
diff --git a/tests/test_server.py b/tests/test_server.py
index e58ce70c..58028859 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -441,7 +441,9 @@ def test_base_instructions_prefer_typed_completion_over_polling() -> None:
     # tool; the instructions must still name it so agents know the
     # "wait for any new output" affordance exists.
     assert "patterns=null" in _BASE_INSTRUCTIONS
-    assert "stop=" in _BASE_INSTRUCTIONS
+    # Phrased as a stop *hit*, not "stop=[] bails" — two readers parsed
+    # the old wording as "the empty list bails", which it does not.
+    assert "stop hit" in _BASE_INSTRUCTIONS
     assert "send_keys_batch" in _BASE_INSTRUCTIONS
     assert _BASE_INSTRUCTIONS.index("run_command") < _BASE_INSTRUCTIONS.index(
         "wait_for_channel"

From e26b884be5cdf11b621b4b83e98397fe6f16fd1e Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:36:28 -0500
Subject: [PATCH 48/71] Mcp(fix[servers]): Give each list_servers row a
 complete identity
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The directory scan held each socket's full path in the entry it was
iterating and reported only its name, so `socket_path` was null on every
scanned row. Passing that same socket through `extra_socket_paths` then
listed it a second time carrying the opposite half of its identity, with
nothing tying the two rows together — an agent could not tell they were
one server.

Scanned rows now carry both fields, and extras are deduplicated against
the scan by resolved path, which also survives symlinks and relative
paths.

A socket whose name does not round-trip through `tmux -L` — one holding
a newline — raised inside `get_server_info` and was dropped from the
listing by a bare `continue`, so a live server vanished without a word.
It now falls back to the path probe, which always works.
---
 CHANGES                               | 13 ++++++++++
 src/libtmux_mcp/tools/server_tools.py | 37 +++++++++++++++++++++++----
 tests/test_server_tools.py            | 35 +++++++++++++++++++++++++
 3 files changed, 80 insertions(+), 5 deletions(-)

diff --git a/CHANGES b/CHANGES
index 8299ed2e..c02ffc28 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,19 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### `list_servers` rows carry a complete identity
+
+The directory scan held each socket's full path and reported only its
+name, so `socket_path` was null on every scanned row. Passing that same
+socket through `extra_socket_paths` then listed it a second time
+carrying the opposite half of its identity, with nothing to tie the two
+rows together — an agent could not tell they were one server.
+
+Scanned rows now carry both fields, and extras are deduplicated against
+the scan by resolved path. A socket whose name does not round-trip
+through `tmux -L` — one containing a newline, say — is listed by path
+instead of being dropped from the results without a word.
+
 #### `wait_for_text` reports a stop marker that was already on screen
 
 The entry scan covered `patterns` and not `stop`, so waiting on a pane
diff --git a/src/libtmux_mcp/tools/server_tools.py b/src/libtmux_mcp/tools/server_tools.py
index 8075101f..1c0b7623 100644
--- a/src/libtmux_mcp/tools/server_tools.py
+++ b/src/libtmux_mcp/tools/server_tools.py
@@ -262,6 +262,14 @@ def _is_tmux_socket_live(path: pathlib.Path) -> bool:
             s.close()
 
 
+def _socket_key(path: pathlib.Path) -> str:
+    """Identity for a socket, stable across symlinks and relative paths."""
+    try:
+        return str(path.resolve())
+    except OSError:
+        return str(path)
+
+
 def _probe_server_by_path(socket_path: pathlib.Path) -> ServerInfo | None:
     """Return a :class:`ServerInfo` for a live socket at ``socket_path``.
 
@@ -360,7 +368,11 @@ def list_servers(
     """
     tmux_tmpdir = os.environ.get("TMUX_TMPDIR", "/tmp")
     uid_dir = pathlib.Path(tmux_tmpdir) / f"tmux-{os.geteuid()}"
-    results: list[ServerInfo] = []
+    # Keyed by resolved socket path so an extra that names a socket the
+    # scan already found is recognized as the same server rather than
+    # listed a second time with a disjoint half of its identity.
+    # Insertion order preserves "scan results first, extras in order".
+    found: dict[str, ServerInfo] = {}
     if uid_dir.is_dir():
         for entry in sorted(uid_dir.iterdir()):
             try:
@@ -372,16 +384,31 @@ def list_servers(
             # ``get_server_info`` call. Stale sockets are the common case.
             if not _is_tmux_socket_live(entry):
                 continue
+            info: ServerInfo | None
             try:
                 info = get_server_info(socket_name=entry.name)
             except ToolError:
+                # A name that does not round-trip through ``-L`` -- one
+                # holding a newline, say -- used to drop a live server
+                # from the listing silently. The path always works.
+                info = _probe_server_by_path(entry)
+            if info is None:
                 continue
-            results.append(info)
+            # The scan holds the full path; reporting only the name left
+            # ``socket_path`` null on every scanned row, so no row
+            # carried a complete identity.
+            found[_socket_key(entry)] = info.model_copy(
+                update={"socket_name": entry.name, "socket_path": str(entry)}
+            )
     for raw_path in extra_socket_paths or []:
-        extra = _probe_server_by_path(pathlib.Path(raw_path))
+        path = pathlib.Path(raw_path)
+        key = _socket_key(path)
+        if key in found:
+            continue
+        extra = _probe_server_by_path(path)
         if extra is not None:
-            results.append(extra)
-    return results
+            found[key] = extra
+    return list(found.values())
 
 
 def register(mcp: FastMCP) -> None:
diff --git a/tests/test_server_tools.py b/tests/test_server_tools.py
index 525668d0..e7bf7358 100644
--- a/tests/test_server_tools.py
+++ b/tests/test_server_tools.py
@@ -387,6 +387,41 @@ def test_list_servers_finds_live_socket(mcp_server: Server) -> None:
     assert found.is_alive is True
 
 
+def test_list_servers_reports_a_complete_identity_and_dedups(
+    mcp_server: Server,
+    mcp_session: Session,
+) -> None:
+    """Each row carries both identity fields, and extras do not duplicate.
+
+    The scan holds the full path in the directory entry but reported
+    only the name, so ``socket_path`` was null on every scanned row.
+    Passing the same socket via ``extra_socket_paths`` then listed it a
+    second time with the opposite half of its identity, and nothing tied
+    the two rows together.
+    """
+    import os
+    import pathlib as _pathlib
+
+    assert mcp_session is not None  # forces the tmux server to exist
+    socket_path = (
+        _pathlib.Path(os.environ.get("TMUX_TMPDIR", "/tmp"))
+        / f"tmux-{os.geteuid()}"
+        / str(mcp_server.socket_name)
+    )
+
+    scanned = [r for r in list_servers() if r.socket_name == mcp_server.socket_name]
+    assert len(scanned) == 1
+    assert scanned[0].socket_path == str(socket_path)
+
+    both = list_servers(extra_socket_paths=[str(socket_path)])
+    same_server = [
+        r
+        for r in both
+        if r.socket_name == mcp_server.socket_name or r.socket_path == str(socket_path)
+    ]
+    assert len(same_server) == 1
+
+
 def test_list_servers_missing_tmpdir_returns_empty(
     monkeypatch: pytest.MonkeyPatch,
 ) -> None:

From 0a13a29e71338e4ab389a5841b7ea7969cf17ef0 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:47:54 -0500
Subject: [PATCH 49/71] Mcp(fix[capture]): Report the loss when a pane laps its
 history limit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Output that laps a pane's `history-limit` returned no lines with
`lines_missed=False`, while tmux still held dozens of them. The field
built to report exactly this loss reported its opposite.

Two defects compounded. `_cursor_anchor_lost` has no overflow case:
`history_size` climbs to the limit and then stays pinned while rows are
evicted off the top, so none of its three tests fire. And the
fingerprint degenerates to a single hash whenever the anchor was the
last row — the normal case, because an agent starts tailing an idle
pane and the anchor is the shell prompt. The uniqueness guard asks
whether a candidate is unique *in the current buffer*, not unique *in
time*, so once the flood evicted the anchor its one surviving twin was
the prompt currently on screen: one candidate, guard satisfied, false
match far below the real anchor. Everything above it was dropped as
"already seen".

Matches are now rejected on position past `anchor_abs` — tmux evicts
only from the top, so a surviving anchor can only move earlier. That is
necessary but not sufficient: an anchor taken at the bottom of an
already-saturated history occupies the same row as the current prompt,
and the two candidates genuinely overlap. So a single-hash fingerprint
on a saturated history additionally refuses to match inside the visible
region. Declining costs a conservative `lines_missed=True`, which stays
honest — a saturated history means rows were evicted whether or not the
anchor itself survived — and panes away from their limit are untouched.

Saturation is asked via the existing trim-risk heuristic, not
`history_size == history_limit`: measured on tmux 3.7c, a pane with
`history-limit 20` pins at `history_size 19`, so an exact comparison
never fires on the very panes this guards.

The existing regression test worked around the defect rather than
catching it. Its docstring records that "the flood alone is not
deterministic — tmux 3.6 retains enough of the original prompt that
`_find_unique_cursor_match` re-anchors on the surviving hash" and adds a
`clear-history` to force anchor destruction, so the flood-only path that
real agents hit was never covered. It is now, without help.

Measured before/after on a 20-line history: a 5-line burst returned 0
lines claiming nothing was missed, and now returns the visible content
flagged `lines_missed=True`; a 50000-line history is unchanged.
---
 CHANGES                                       | 37 +++++++++++
 .../tools/pane_tools/capture_since.py         | 59 +++++++++++++++--
 tests/test_pane_tools.py                      | 63 +++++++++++++++++++
 3 files changed, 155 insertions(+), 4 deletions(-)

diff --git a/CHANGES b/CHANGES
index c02ffc28..ca2bf42f 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,43 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### `capture_since` no longer loses a flooded pane silently
+
+Output that laps a pane's `history-limit` returned **no lines** with
+`lines_missed=False`, while tmux still held dozens of them. The field
+built to report exactly this loss reported the opposite.
+
+Two defects compounded. `_cursor_anchor_lost` has no overflow case:
+`history_size` climbs to the limit and then stays pinned while rows are
+evicted off the top, so none of its three tests fire. And the cursor
+fingerprint degenerates to a single hash whenever the anchor was the
+last row — which is the normal case, because an agent starts tailing an
+idle pane and the anchor is the shell prompt. The uniqueness guard asks
+whether a candidate is unique *in the current buffer*, not unique *in
+time*, so once the flood evicted the anchor its one surviving twin was
+the prompt currently on screen: one candidate, guard satisfied, false
+match. Everything above it was dropped as "already seen".
+
+Matches are now rejected on position when they fall past `anchor_abs` —
+tmux evicts only from the top, so a surviving anchor can only move
+earlier. Where that is not enough (an anchor taken at the bottom of an
+already-saturated history, where the two positions overlap), a
+single-hash fingerprint additionally refuses to match inside the
+visible region. Declining costs a conservative `lines_missed=True`,
+which stays honest: a saturated history means rows were evicted whether
+or not the anchor survived. Panes not near their history limit are
+unaffected.
+
+Saturation is asked via the existing trim-risk heuristic rather than
+`history_size == history_limit`: measured on tmux 3.7c, a pane with
+`history-limit 20` pins at `history_size 19`, so an exact comparison
+never fires on the very panes this guards.
+
+The existing regression test worked around the defect rather than
+catching it — its docstring notes that "the flood alone is not
+deterministic" and adds a `clear-history` to force anchor destruction,
+so the flood-only path real agents hit was never covered. It is now.
+
 #### `list_servers` rows carry a complete identity
 
 The directory scan held each socket's full path and reported only its
diff --git a/src/libtmux_mcp/tools/pane_tools/capture_since.py b/src/libtmux_mcp/tools/pane_tools/capture_since.py
index 5d6f77de..7c4314ee 100644
--- a/src/libtmux_mcp/tools/pane_tools/capture_since.py
+++ b/src/libtmux_mcp/tools/pane_tools/capture_since.py
@@ -182,8 +182,40 @@ def _history_limit_trim_risk(
     return cursor.history_size >= risk_floor or state.history_size >= risk_floor
 
 
-def _find_unique_cursor_match(rows: list[str], cursor: _CaptureCursor) -> int | None:
-    """Find one retained row sequence matching the cursor fingerprint."""
+def _find_unique_cursor_match(
+    rows: list[str],
+    cursor: _CaptureCursor,
+    state: _PaneState,
+    history_limit: int,
+) -> int | None:
+    """Find one retained row sequence matching the cursor fingerprint.
+
+    *rows* comes from ``capture-pane -S -``, so ``rows[i]`` is absolute
+    grid row ``i``. tmux evicts only from the TOP, so a surviving anchor
+    can only have moved EARLIER than ``cursor.anchor_abs``, never later.
+    Candidates past that row are therefore rejected on position alone,
+    however well they hash.
+
+    That bound is what makes this safe when the fingerprint degenerates
+    to a single hash (``below_hashes`` empty, i.e. the anchor was the
+    last row). The uniqueness rule below asks whether a candidate is
+    unique *in the current buffer*, not unique *in time* — and an agent
+    that starts tailing an idle pane anchors on the shell prompt, a line
+    that recurs verbatim after every command. Once enough output laps
+    the history limit, the evicted anchor's only surviving twin is the
+    CURRENT prompt near the bottom: exactly one candidate, so the
+    uniqueness guard passed and returned a false match far below the
+    real anchor. Everything above it was then dropped as "already seen"
+    and the read reported ``lines_missed=False`` having lost the lot.
+
+    Position alone is necessary but not sufficient: when the anchor was
+    taken near the bottom of an already-full history, its old row and
+    the current prompt's row overlap. So a single-hash fingerprint on a
+    full history additionally refuses to match inside the visible
+    region. Declining costs only a conservative ``lines_missed=True``,
+    which stays honest: a full history means rows above the anchor were
+    evicted whether or not the anchor itself survived.
+    """
     if cursor.anchor_hash is None:
         return None
 
@@ -191,8 +223,25 @@ def _find_unique_cursor_match(rows: list[str], cursor: _CaptureCursor) -> int |
     if len(rows) < len(fingerprint):
         return None
 
+    # A one-row fingerprint carries no way to tell the anchor from any
+    # other line with the same text, and on a SATURATED history the twin
+    # that survives is the prompt currently on screen.
+    # ``index >= history_size`` means the candidate sits in the visible
+    # region rather than in scrollback, which is that signature exactly.
+    #
+    # Saturation is asked via ``_history_limit_trim_risk`` rather than
+    # ``history_size == history_limit``: measured on tmux 3.7c, a pane
+    # with ``history-limit 20`` pins at ``history_size 19``, so an exact
+    # comparison never fires on the very panes this guards.
+    blind = len(fingerprint) == 1 and _history_limit_trim_risk(
+        cursor, state, history_limit
+    )
+
     match_index: int | None = None
-    for index in range(len(rows) - len(fingerprint) + 1):
+    last_possible = min(len(rows) - len(fingerprint), cursor.anchor_abs)
+    for index in range(last_possible + 1):
+        if blind and index >= state.history_size:
+            continue
         candidate = rows[index : index + len(fingerprint)]
         candidate_hashes = tuple(_line_hash(line) for line in candidate)
         if candidate_hashes != fingerprint:
@@ -259,7 +308,9 @@ def _read_delta(pane: Pane, cursor: _CaptureCursor) -> _PaneRead:
         _raise_if_pane_lifecycle_changed(pane.pane_id, after, cursor.pane_pid)
         if _same_state(before, after):
             if trim_risk:
-                match_index = _find_unique_cursor_match(rows, cursor)
+                match_index = _find_unique_cursor_match(
+                    rows, cursor, before, history_limit
+                )
                 if match_index is None:
                     missed = _read_stable_visible(pane, baseline_pid=cursor.pane_pid)
                     return _PaneRead(
diff --git a/tests/test_pane_tools.py b/tests/test_pane_tools.py
index ff3e6a1d..6660afc4 100644
--- a/tests/test_pane_tools.py
+++ b/tests/test_pane_tools.py
@@ -1526,6 +1526,69 @@ def _hlimit_locked() -> bool:
         fresh_pane.kill()
 
 
+def test_capture_since_reports_overflow_without_clear_history(
+    mcp_server: Server, mcp_pane: Pane
+) -> None:
+    """Output lapping ``history-limit`` reports the loss on its own.
+
+    The sibling test above needs an explicit ``clear-history`` because
+    the flood alone used to be non-deterministic: the evicted anchor is
+    the shell prompt, a line that recurs verbatim after every command,
+    so its only surviving twin was the CURRENT prompt near the bottom.
+    One candidate passed the uniqueness guard, everything above it was
+    dropped as "already seen", and the read returned no lines while
+    reporting ``lines_missed=False``.
+
+    Rows are only evicted from the top, so a surviving anchor can only
+    move earlier than ``anchor_abs``; a match past it is rejected on
+    position. That makes the flood-only path -- the one real agents hit
+    while tailing a build -- deterministic, so it is tested here
+    without help.
+    """
+    import asyncio
+
+    mcp_pane.session.cmd("set-option", "-g", "history-limit", "20")
+    fresh_pane = mcp_pane.window.split()
+    assert fresh_pane.pane_id is not None
+
+    def _hlimit_locked() -> bool:
+        raw = fresh_pane.display_message("#{history_limit}", get_text=True)
+        return bool(raw) and int(raw[0]) == 20
+
+    try:
+        retry_until(_hlimit_locked, 5, raises=True)
+        _signal_after_shell_payload(
+            mcp_server,
+            fresh_pane,
+            "for i in $(seq 1 25); do printf 'OVF_PRE_%03d\\n' \"$i\"; done",
+        )
+        first = asyncio.run(
+            capture_since(
+                pane_id=fresh_pane.pane_id,
+                socket_name=mcp_server.socket_name,
+            )
+        )
+
+        _signal_after_shell_payload(
+            mcp_server,
+            fresh_pane,
+            "for i in $(seq 1 300); do printf 'OVF_%03d\\n' \"$i\"; done",
+        )
+        second = asyncio.run(
+            capture_since(
+                cursor=first.cursor,
+                socket_name=mcp_server.socket_name,
+            )
+        )
+
+        # 300 lines through a 20-line history: rows were destroyed, and
+        # the read must say so rather than returning an empty success.
+        assert second.lines_missed is True
+        assert second.lines
+    finally:
+        fresh_pane.kill()
+
+
 def test_capture_since_reports_same_row_rewrite(
     mcp_server: Server, mcp_pane: Pane
 ) -> None:

From 815c299dce0539b83dbbc98a94070b35d4fce2be Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:47:54 -0500
Subject: [PATCH 50/71] Mcp(fix[hooks]): Merge both hook trees on the default
 scope too
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

tmux does not unify `-g` listings across its session and window trees,
so `show-hooks -g` omits pane-level hooks that `show-hooks -gw` holds.
A merge existed to paper over that and its own comment said so — but it
was gated on the caller passing `scope="server"` explicitly.

The natural call, `show_hooks()`, leaves `scope` at its default of
`None` and skipped the merge, so asking "what hooks are configured?"
the obvious way returned an incomplete list with no sign anything was
omitted, and `show_hook()` on a missing name then contradicted it. The
server instructions point agents straight at that pair.

The test named for this behavior only exercised the explicit-scope
path, so the default one — the one agents actually take — was never
covered.
---
 CHANGES                             | 14 ++++++++++++++
 src/libtmux_mcp/tools/hook_tools.py | 10 +++++++---
 tests/test_hook_tools.py            | 12 ++++++++++++
 3 files changed, 33 insertions(+), 3 deletions(-)

diff --git a/CHANGES b/CHANGES
index ca2bf42f..9d3bdd74 100644
--- a/CHANGES
+++ b/CHANGES
@@ -45,6 +45,20 @@ catching it — its docstring notes that "the flood alone is not
 deterministic" and adds a `clear-history` to force anchor destruction,
 so the flood-only path real agents hit was never covered. It is now.
 
+#### `show_hooks()` no longer drops hooks that `show_hook()` finds
+
+tmux does not unify `-g` listings across its session and window trees,
+so `show-hooks -g` omits pane-level hooks that `show-hooks -gw` holds.
+A merge existed to paper over that, and its own comment said what it
+was for — but it was gated on the caller passing `scope="server"`
+explicitly. The natural call, `show_hooks()`, left `scope` at its
+default and skipped the merge, so asking "what hooks are configured?"
+the obvious way returned an incomplete list with no sign anything was
+omitted, and `show_hook()` on a missing name then contradicted it.
+
+The test named for this behavior only exercised the explicit-scope
+path, so the default one was never covered.
+
 #### `list_servers` rows carry a complete identity
 
 The directory scan held each socket's full path and reported only its
diff --git a/src/libtmux_mcp/tools/hook_tools.py b/src/libtmux_mcp/tools/hook_tools.py
index c7821dad..0b3dfd24 100644
--- a/src/libtmux_mcp/tools/hook_tools.py
+++ b/src/libtmux_mcp/tools/hook_tools.py
@@ -198,13 +198,17 @@ def show_hooks(
     obj, opt_scope = _resolve_hook_target(socket_name, scope, target)
     raw: dict[str, t.Any] = obj.show_hooks(global_=global_, scope=opt_scope)
 
-    if scope == "server" and target is None:
+    if target is None and scope in (None, "server"):
         # Also consult the global-window options tree. tmux doesn't
         # unify ``-g`` listings across the session and window trees;
         # ``show-hooks -g`` alone misses pane/window-level globals.
         # Without this merge, ``show_hook(name)`` would find a hook
-        # that ``show_hooks()`` silently drops — the inconsistency
-        # guarded by ``test_show_hooks_surfaces_globally_set_pane_hook``.
+        # that ``show_hooks()`` silently drops.
+        #
+        # ``scope=None`` must be included: it is the DEFAULT, so the
+        # obvious call — ``show_hooks()`` — skipped the merge entirely
+        # and reproduced the very inconsistency this block exists to
+        # prevent. Only an explicit ``scope="server"`` got the fix.
         raw_window = obj.show_hooks(global_=True, scope=OptionScope.Window)
         for name, value in raw_window.items():
             raw.setdefault(name, value)
diff --git a/tests/test_hook_tools.py b/tests/test_hook_tools.py
index 4545c3ed..2fa96c83 100644
--- a/tests/test_hook_tools.py
+++ b/tests/test_hook_tools.py
@@ -125,6 +125,12 @@ def test_show_hooks_surfaces_globally_set_pane_hook(
             scope="server",
             socket_name=mcp_server.socket_name,
         )
+        # The DEFAULT call shape, which is what an agent actually
+        # writes. The merge was gated on an explicit scope="server", so
+        # this path skipped it and reproduced the very inconsistency the
+        # merge exists to prevent -- and this test, named for that
+        # behavior, never exercised it.
+        defaulted = show_hooks(socket_name=mcp_server.socket_name)
 
         # Control: show_hook finds the -g-set pane hook.
         singular_names = {e.hook_name for e in singular.entries}
@@ -138,6 +144,12 @@ def test_show_hooks_surfaces_globally_set_pane_hook(
             f"show_hooks(scope='server') returned {plural_names} "
             f"but show_hook found pane-focus-in — inconsistency."
         )
+
+        defaulted_names = {e.hook_name for e in defaulted.entries}
+        assert "pane-focus-in" in defaulted_names, (
+            f"show_hooks() returned {defaulted_names} but show_hook found "
+            f"pane-focus-in — the default scope must merge both trees too."
+        )
     finally:
         mcp_server.cmd("set-hook", "-g", "-u", "pane-focus-in")
     _ = mcp_session  # session fixture ensures the server has a usable target

From 836ae624fbd198dd9130074ce8ce46efdb0fd9f7 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:53:26 -0500
Subject: [PATCH 51/71] Mcp(fix[limits]): Keep a truncated success schema-valid
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A capture large enough to hit the 1 MB backstop produced `RuntimeError:
Tool capture_pane has an output schema but did not return structured
content` — a transport-level failure delivering no data at all, which
is worse than the truncation the limiter exists to perform and worse
than having no limiter. `capture_pane`'s docstring advertises
`max_lines=None` for a complete capture, so the documented way to ask
for everything was the way to break it. Size-driven, not `None`-driven:
a large explicit `max_lines` fails identically.

The limiter rebuilds the result when it truncates, and that rebuild
dropped `structured_content` alongside `is_error`. The `is_error` half
was already fixed, and this class's own docstring spells out why —
"MCP clients then validate the truncated text against the tool's output
schema and fail with a transport-level error". The successful-response
half was left standing with the same consequence and no `is_error` to
restore.

Truncated successes now carry structured content. Only the
`{"result": str}` shape fastmcp gives a `-> str` tool is rebuildable;
model- and list-shaped payloads carry the oversize inside their own
fields and cannot be trimmed from the flattened text, so those return
an actionable tool error telling the agent to narrow its range rather
than a response its client will reject outright.

Verified against a real >1 MB capture: `max_lines=None` returns 954 KB
of truncated data with structured content intact instead of raising.
Tail preservation, the dropped-line count, and the per-tool caps were
correct throughout; only the over-cap rebuild was broken.
---
 CHANGES                       | 23 ++++++++++++
 src/libtmux_mcp/middleware.py | 63 +++++++++++++++++++++++++++++--
 tests/test_middleware.py      | 71 +++++++++++++++++++++++++++++++++++
 3 files changed, 153 insertions(+), 4 deletions(-)

diff --git a/CHANGES b/CHANGES
index 9d3bdd74..8638b798 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,29 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### An oversized response is truncated, not rejected by the client
+
+A capture large enough to hit the server's 1 MB backstop produced
+`RuntimeError: Tool capture_pane has an output schema but did not return
+structured content` — a transport-level failure delivering **no data at
+all**, which is worse than the truncation the limiter exists to perform.
+`capture_pane`'s own docstring advertises `max_lines=None` for a
+complete capture, so the documented way to ask for everything was the
+way to break it. It is size-driven, not `None`-driven: a large explicit
+`max_lines` fails identically.
+
+The limiter rebuilds the result when it truncates, and that rebuild
+dropped `structured_content` alongside `is_error`. The `is_error` half
+had already been fixed and documented; the successful-response half was
+left standing. Truncated successes now carry structured content again.
+
+Where the payload's shape cannot be trimmed while staying schema-valid,
+the call returns an actionable tool error telling the agent to narrow
+its range, rather than a response its client will reject outright.
+
+Tail preservation itself was correct throughout — the head is dropped,
+the newest output kept, and the number of dropped lines reported.
+
 #### `capture_since` no longer loses a flooded pane silently
 
 Output that laps a pane's `history-limit` returned **no lines** with
diff --git a/src/libtmux_mcp/middleware.py b/src/libtmux_mcp/middleware.py
index bde3be84..d1288e04 100644
--- a/src/libtmux_mcp/middleware.py
+++ b/src/libtmux_mcp/middleware.py
@@ -909,6 +909,34 @@ async def on_call_tool(
 _TRUNCATION_HEADER_TEMPLATE = "[... truncated {dropped} bytes ...]\n"
 
 
+def _restructure_truncated(
+    original: dict[str, t.Any],
+    truncated: ToolResult,
+) -> ToolResult | None:
+    """Re-attach structured content to a truncated success result.
+
+    Only the single-string result shape is rebuildable: fastmcp wraps a
+    ``-> str`` tool as ``{"result": "..."}``, so swapping in the
+    truncated text keeps the payload schema-valid. Model- and
+    list-shaped results carry the oversize inside their own fields and
+    cannot be trimmed from the flattened text, so they return None and
+    the caller reports a tool error instead of an invalid response.
+    """
+    if set(original) != {"result"} or not isinstance(original["result"], str):
+        return None
+    text = next(
+        (block.text for block in truncated.content if isinstance(block, TextContent)),
+        None,
+    )
+    if text is None:
+        return None
+    return ToolResult(
+        content=truncated.content,
+        structured_content={"result": text},
+        meta=truncated.meta,
+    )
+
+
 class TailPreservingResponseLimitingMiddleware(ResponseLimitingMiddleware):
     """Response-limiter that keeps the tail of oversized output.
 
@@ -957,15 +985,42 @@ async def _capture(
             return t.cast("ToolResult", inner)
 
         result = await super().on_call_tool(context, _capture)
-        if result is not inner and isinstance(inner, ToolResult) and inner.is_error:
-            # The base class truncated and rebuilt the result; restore
-            # the error flag it dropped.
+        if result is inner or not isinstance(inner, ToolResult):
+            return result
+
+        # The base class truncated and rebuilt the result, dropping both
+        # ``is_error`` and ``structured_content``.
+        if inner.is_error:
             return ToolResult(
                 content=result.content,
                 meta=result.meta,
                 is_error=True,
             )
-        return result
+        if inner.structured_content is None:
+            return result
+
+        # A SUCCESSFUL oversized response is the other half of the same
+        # defect: the tool declares an output schema, the rebuilt result
+        # carries no structured content, and a spec-compliant client
+        # raises a transport-level error instead of delivering truncated
+        # data. That is worse than the truncation this middleware exists
+        # to perform, and worse than having no middleware at all.
+        rebuilt = _restructure_truncated(inner.structured_content, result)
+        if rebuilt is not None:
+            return rebuilt
+        # Shape we cannot rebuild: say so as a tool error the agent can
+        # act on, rather than emitting a response its client will reject.
+        msg = (
+            "Response exceeded the server's size limit and could not be "
+            "truncated while satisfying this tool's output schema. Re-run "
+            "with a narrower range (for example a smaller max_lines, or a "
+            "less negative start)."
+        )
+        return ToolResult(
+            content=[TextContent(type="text", text=msg)],
+            meta=result.meta,
+            is_error=True,
+        )
 
     def _truncate_to_result(
         self,
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index a20d2458..9b8672aa 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -801,6 +801,77 @@ def test_tail_preserving_passthrough_when_under_cap() -> None:
     assert result.content[0].text == payload
 
 
+def _limiter_context(tool_name: str) -> t.Any:
+    """Minimal MiddlewareContext naming the tool the limiter should cap."""
+    return MiddlewareContext(
+        message=CallToolRequestParams(name=tool_name, arguments={}),
+        fastmcp_context=None,
+    )
+
+
+def test_tail_preserving_keeps_structured_content_on_success() -> None:
+    """A truncated SUCCESS must still satisfy the tool's output schema.
+
+    The rebuilt result carried ``content`` only. For a tool declaring an
+    output schema, a spec-compliant client then raises "has an output
+    schema but did not return structured content" and the agent gets no
+    data at all — worse than the truncation this middleware exists to
+    perform. The error branch had already been fixed; the success branch
+    had not.
+    """
+    from fastmcp.tools.base import ToolResult
+    from mcp.types import TextContent
+
+    from libtmux_mcp.middleware import TailPreservingResponseLimitingMiddleware
+
+    payload = ("HEAD_OLDER\n" * 500) + "TAIL_PROMPT $"
+    mw = TailPreservingResponseLimitingMiddleware(max_size=400, tools=["cap"])
+
+    async def _call_next(_ctx: t.Any) -> ToolResult:
+        return ToolResult(
+            content=[TextContent(type="text", text=payload)],
+            structured_content={"result": payload},
+        )
+
+    ctx = _limiter_context("cap")
+    result = asyncio.run(mw.on_call_tool(ctx, _call_next))
+
+    assert result.structured_content is not None
+    assert set(result.structured_content) == {"result"}
+    text = result.structured_content["result"]
+    # Structured payload matches the truncated text, tail preserved.
+    assert text == result.content[0].text
+    assert "TAIL_PROMPT $" in text
+    assert result.is_error is False
+
+
+def test_tail_preserving_reports_an_unrebuildable_shape_as_a_tool_error() -> None:
+    """A shape that cannot be trimmed becomes an actionable tool error.
+
+    Better than emitting a response the client will reject outright: the
+    agent learns to narrow its request.
+    """
+    from fastmcp.tools.base import ToolResult
+    from mcp.types import TextContent
+
+    from libtmux_mcp.middleware import TailPreservingResponseLimitingMiddleware
+
+    payload = "x" * 5000
+    mw = TailPreservingResponseLimitingMiddleware(max_size=400, tools=["cap"])
+
+    async def _call_next(_ctx: t.Any) -> ToolResult:
+        return ToolResult(
+            content=[TextContent(type="text", text=payload)],
+            structured_content={"lines": [payload], "truncated": False},
+        )
+
+    ctx = _limiter_context("cap")
+    result = asyncio.run(mw.on_call_tool(ctx, _call_next))
+
+    assert result.is_error is True
+    assert "narrower range" in result.content[0].text
+
+
 # ---------------------------------------------------------------------------
 # Middleware stack composition tests
 # ---------------------------------------------------------------------------

From 0778479acd9b0cc7faebbc403aa2d565b8aad578 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 20:58:40 -0500
Subject: [PATCH 52/71] Mcp(fix[search]): Say how much of each pane was read
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

`search_panes` searches only the visible screen unless `content_start`
is given, but reported `matches: []` alongside `truncated: false` — an
active claim that nothing was left out, for a search that never looked
at scrollback. Its docstring described "visible terminal scrollback
content", which reads as scrollback and is what led two readers to
expect it.

The result now carries `searched_scope`, and `truncated` is documented
as describing the `limit` and per-pane line caps only. The default
stays visible-only deliberately: the tool fans out across every pane on
the server, so defaulting to scrollback would multiply cost by history
depth times pane count and make identical calls take wildly different
times depending on how long panes had been alive. Telling the agent
what was searched is strictly more useful than a slow complete answer,
because it also reveals the knob.

Also: `paste_text("")` errored with `no buffer libtmux_mcp_..._paste`
because tmux creates no buffer for empty content — an error for a
no-op, naming an internal buffer the caller never chose. It is now a
no-op success.

And `send_keys` documents the roughly 16 KB tmux ceiling that surfaces
as `command too long`, pointing at `paste_text` for larger payloads,
plus a warning against verifying a write by string-comparing captured
text: tmux renders combining marks and zero-width joiners as ``
placeholders, so `école` and emoji come back transformed even when the
bytes were delivered correctly.
---
 CHANGES                                    | 32 ++++++++++++++++++++++
 src/libtmux_mcp/models.py                  | 15 +++++++++-
 src/libtmux_mcp/tools/pane_tools/io.py     | 17 ++++++++++++
 src/libtmux_mcp/tools/pane_tools/search.py | 20 ++++++++++++--
 4 files changed, 80 insertions(+), 4 deletions(-)

diff --git a/CHANGES b/CHANGES
index 8638b798..7532594f 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,38 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### `search_panes` says how much of each pane it read
+
+`search_panes` searches only the visible screen unless `content_start`
+is given, but reported `matches: []` alongside `truncated: false` — an
+active claim that nothing was left out, for a search that never looked
+at scrollback. Its docstring described "visible terminal scrollback
+content", which reads as scrollback.
+
+The result now carries `searched_scope` (`visible` / `scrollback`), and
+`truncated` is documented as describing the `limit` and per-pane line
+caps only. The default stays visible-only deliberately: the tool fans
+out across every pane on the server, so defaulting to scrollback would
+multiply cost by history depth times pane count and make identical
+calls take wildly different times depending on how long panes had been
+alive.
+
+#### `paste_text("")` is a no-op instead of an error
+
+tmux creates no buffer for empty content, so the follow-up
+`paste-buffer` failed with `no buffer libtmux_mcp_..._paste` — an error
+for a no-op, naming an internal buffer the caller never chose.
+
+#### `send_keys` documents its size ceiling
+
+tmux rejects a `send-keys` argument beyond roughly 16 KB with `command
+too long`. The docstring now names that limit and points at
+`paste_text`, which routes through a buffer rather than argv and takes
+far more. It also warns against verifying a write by string-comparing
+captured text: tmux renders combining marks and zero-width joiners as
+`` placeholders, so `école` and emoji sequences come back
+transformed even when the bytes were delivered correctly.
+
 #### An oversized response is truncated, not rejected by the client
 
 A capture large enough to hit the server's 1 MB backstop produced
diff --git a/src/libtmux_mcp/models.py b/src/libtmux_mcp/models.py
index 3bcb38b1..f2750321 100644
--- a/src/libtmux_mcp/models.py
+++ b/src/libtmux_mcp/models.py
@@ -666,11 +666,24 @@ class SearchPanesResult(BaseModel):
         default_factory=list,
         description="PaneContentMatch entries for this page.",
     )
+    searched_scope: t.Literal["visible", "scrollback"] = Field(
+        default="visible",
+        description=(
+            "How much of each pane was read. ``visible`` is the default "
+            "and means ONLY the on-screen rows were searched, so a match "
+            "that has scrolled off is not reported and ``matches: []`` "
+            "does not mean the text is absent. Pass ``content_start`` "
+            "(e.g. -500) to search scrollback."
+        ),
+    )
     truncated: bool = Field(
         default=False,
         description=(
             "True when the result set was truncated by ``limit`` or "
-            "by ``max_matched_lines_per_pane`` on any pane."
+            "by ``max_matched_lines_per_pane`` on any pane. It describes "
+            "those caps ONLY -- it never reports rows left unread "
+            "because the search was scoped to the visible screen; read "
+            "``searched_scope`` for that."
         ),
     )
     truncated_panes: list[str] = Field(
diff --git a/src/libtmux_mcp/tools/pane_tools/io.py b/src/libtmux_mcp/tools/pane_tools/io.py
index e16cd818..a778b2fe 100644
--- a/src/libtmux_mcp/tools/pane_tools/io.py
+++ b/src/libtmux_mcp/tools/pane_tools/io.py
@@ -199,6 +199,16 @@ def send_keys(
     Do NOT call ``capture_pane`` immediately — both the read and the
     pattern-match paths race the pane's PTY draw.
 
+    **Size limit:** tmux rejects a ``send-keys`` argument beyond roughly
+    16 KB with ``command too long``. ``paste_text`` routes through a
+    buffer instead of argv and takes far more, so use it for large
+    payloads.
+
+    **Verifying a write:** do not string-compare captured text against
+    what you sent. tmux renders combining marks and zero-width joiners
+    as ```` placeholders, so ``école`` and emoji sequences come
+    back transformed even though the bytes were delivered correctly.
+
     Parameters
     ----------
     keys : str
@@ -842,6 +852,13 @@ def paste_text(
         window_id=window_id,
     )
 
+    if not text:
+        # tmux creates no buffer for empty content, so the follow-up
+        # paste-buffer failed with "no buffer libtmux_mcp_..._paste" --
+        # an error for a no-op, naming an internal buffer the caller
+        # never chose. Pasting nothing succeeds and does nothing.
+        return f"Text pasted to pane {pane.pane_id}"
+
     # Use a unique named tmux buffer so we don't clobber the user's
     # unnamed paste buffer, and so we can reliably clean up on error
     # paths (paste-buffer -b NAME -d deletes the named buffer). The
diff --git a/src/libtmux_mcp/tools/pane_tools/search.py b/src/libtmux_mcp/tools/pane_tools/search.py
index 21569e4a..1c728a70 100644
--- a/src/libtmux_mcp/tools/pane_tools/search.py
+++ b/src/libtmux_mcp/tools/pane_tools/search.py
@@ -88,9 +88,18 @@ def search_panes(
     """Search visible terminal text across all tmux panes.
 
     Use when the user asks what panes 'contain', 'mention', or 'show' —
-    e.g. 'find the pane with the pytest failure'. Searches each pane's
-    visible terminal scrollback content (not editor or browser text)
-    and returns panes where the pattern is found, with matching lines.
+    e.g. 'find the pane with the pytest failure'. Returns panes where
+    the pattern is found, with matching lines (tmux panes only, not
+    editor or browser text).
+
+    **Scope: the visible screen only, by default.** Scrollback is NOT
+    searched unless ``content_start`` is given, so a match that has
+    already scrolled off returns ``matches: []`` — which does not mean
+    the text is absent. The result reports ``searched_scope`` so this is
+    visible at the call site; pass ``content_start=-500`` (or further)
+    to include scrollback. It stays opt-in because this tool fans out
+    across every pane on the server, and defaulting to scrollback would
+    multiply cost by history depth times pane count.
 
     Bounded output contract
     -----------------------
@@ -285,6 +294,11 @@ def search_panes(
 
     return SearchPanesResult(
         matches=page_matches,
+        searched_scope=(
+            "scrollback"
+            if (content_start is not None or content_end is not None)
+            else "visible"
+        ),
         truncated=per_pane_truncated or global_truncated,
         truncated_panes=skipped_panes,
         total_panes_matched=total_panes_matched,

From fd33c337ab21c36b7f1a2ea76a641f1f268e0393 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 21:05:43 -0500
Subject: [PATCH 53/71] Mcp(feat[options]): Answer what an option is actually
 set to
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

`show_option(option="history-limit", scope="session")` returned
`value: null` while 50000 was in force. tmux resolves inherited values
with `-A` and libtmux has always accepted `include_inherited`, but the
tool never exposed it — so an agent could ask "is this set at this
exact scope?" and never "what is in force here?", which is what a
question like "is mouse mode on?" actually means.

`include_inherited` is now a parameter, and the result carries
`scope_queried` so a `null` reads as "not set at THIS scope" rather
than "not set anywhere".

`show_environment` no longer encodes removal in the key. tmux prints a
removed variable as `-NAME`; the dash was kept and the value set to
boolean `true`, so `variables["KRB5CCNAME"]` raised `KeyError` while
`variables["-KRB5CCNAME"]` answered `true` — the inverse of the truth,
for a variable that is explicitly unset — and every consumer had to
type-check a `str | bool` mapping. `variables` now holds only names
that are set, mapped to their values, with removed names listed
separately.

`search_panes` rejects `offset < 0` and `limit < 1` instead of clamping
or answering with an empty page. `limit=0` returned `matches: []`,
which an agent cannot tell from a genuine miss, and a negative offset
was clamped to zero while being echoed back unchanged.

Also records the measured false-positive band for the capture_since
saturation guard: on a 50000-line limit it reports a loss from roughly
92% full, and is clean at 0/10/50/80/88%. Growth in `history_size`
would narrow it, but a burst that saturates midway both grows and
evicts, so trusting growth would reopen the silent loss that guard
closes.
---
 CHANGES                                       | 32 +++++++++++++
 src/libtmux_mcp/models.py                     | 34 +++++++++++++-
 src/libtmux_mcp/tools/env_tools.py            | 16 ++++++-
 src/libtmux_mcp/tools/option_tools.py         | 27 +++++++++--
 .../tools/pane_tools/capture_since.py         |  9 ++++
 src/libtmux_mcp/tools/pane_tools/search.py    | 16 ++++++-
 tests/test_env_tools.py                       | 24 ++++++++++
 tests/test_option_tools.py                    | 34 ++++++++++++++
 tests/test_pane_tools.py                      | 45 +++++++++++++++++++
 9 files changed, 229 insertions(+), 8 deletions(-)

diff --git a/CHANGES b/CHANGES
index 7532594f..179af9d6 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,38 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### `show_option` can answer what is actually in force
+
+`show_option(option="history-limit", scope="session")` returned
+`value: null` while 50000 was in effect. tmux resolves inherited values
+with `-A` and libtmux has always accepted `include_inherited`, but the
+tool never exposed it — so an agent could ask "is this set at this
+exact scope?" and never "what is in force here?", which is what a
+question like "is mouse mode on?" actually means.
+
+`include_inherited` is now a parameter, and the result carries
+`scope_queried` so a `null` is readable rather than being mistaken for
+"not set anywhere".
+
+#### `show_environment` no longer encodes removal in the key
+
+tmux prints a variable it has marked removed as `-NAME`. The dash was
+kept in the key and the value set to boolean `true`, so
+`variables["KRB5CCNAME"]` raised `KeyError` while
+`variables["-KRB5CCNAME"]` answered `true` — reading as "set to true"
+for a variable that is explicitly unset, and forcing every consumer to
+type-check a `str | bool` mapping.
+
+`variables` now holds only names that are set, mapped to their values,
+and removed names are listed separately under `removed`.
+
+#### `search_panes` rejects pagination it cannot honour
+
+`limit=0` returned `matches: []`, indistinguishable from a genuine
+miss, and a negative `offset` was clamped to zero while being echoed
+back unchanged, so the result silently did not describe the request.
+Both now raise.
+
 #### `search_panes` says how much of each pane it read
 
 `search_panes` searches only the visible screen unless `content_start`
diff --git a/src/libtmux_mcp/models.py b/src/libtmux_mcp/models.py
index f2750321..0b9f9d81 100644
--- a/src/libtmux_mcp/models.py
+++ b/src/libtmux_mcp/models.py
@@ -215,7 +215,26 @@ class OptionResult(BaseModel):
     """Result of a show_option call."""
 
     option: str = Field(description="Option name")
-    value: t.Any = Field(description="Option value")
+    value: t.Any = Field(
+        description=(
+            "Option value. ``null`` means NOT SET AT THE SCOPE QUERIED, "
+            "which is not the same as not set: an option inherited from "
+            "a wider scope reads as null unless ``include_inherited`` "
+            "was passed."
+        )
+    )
+    scope_queried: str = Field(
+        default="server",
+        description="Scope this answer describes, so null is readable.",
+    )
+    include_inherited: bool = Field(
+        default=False,
+        description=(
+            "True when inherited values were resolved (tmux ``-A``), so "
+            "the value is the one in force rather than only one set at "
+            "this scope."
+        ),
+    )
 
 
 class OptionSetResult(BaseModel):
@@ -229,7 +248,18 @@ class OptionSetResult(BaseModel):
 class EnvironmentResult(BaseModel):
     """Result of a show_environment call."""
 
-    variables: dict[str, str | bool] = Field(description="Environment variable mapping")
+    variables: dict[str, str] = Field(
+        description="Variables that are SET, mapped to their values."
+    )
+    removed: list[str] = Field(
+        default_factory=list,
+        description=(
+            "Names tmux marks as explicitly REMOVED from the environment "
+            "(it prints them as ``-NAME``). These are not in ``variables``. "
+            "Distinct from a name that simply never appears, which tmux "
+            "does not report at all."
+        ),
+    )
 
 
 class EnvironmentSetResult(BaseModel):
diff --git a/src/libtmux_mcp/tools/env_tools.py b/src/libtmux_mcp/tools/env_tools.py
index 16407c4e..15a4c252 100644
--- a/src/libtmux_mcp/tools/env_tools.py
+++ b/src/libtmux_mcp/tools/env_tools.py
@@ -55,7 +55,21 @@ def show_environment(
     else:
         env_dict = server.show_environment()
 
-    return EnvironmentResult(variables=env_dict)
+    # tmux prints a variable marked REMOVED as ``-NAME``
+    # (cmd-show-environment.c). libtmux keeps the dash in the key and
+    # gives it the value True, so the removal was encoded in the key --
+    # ``variables["KRB5CCNAME"]`` raised KeyError while
+    # ``variables["-KRB5CCNAME"]`` answered True, reading as "set to
+    # true" for a variable that is explicitly unset. Split the two
+    # apart instead, so a value is always a value.
+    variables: dict[str, str] = {}
+    removed: list[str] = []
+    for name, value in env_dict.items():
+        if name.startswith("-"):
+            removed.append(name[1:])
+        elif isinstance(value, str):
+            variables[name] = value
+    return EnvironmentResult(variables=variables, removed=sorted(removed))
 
 
 @handle_tool_errors
diff --git a/src/libtmux_mcp/tools/option_tools.py b/src/libtmux_mcp/tools/option_tools.py
index e992fd8f..5f7ae56c 100644
--- a/src/libtmux_mcp/tools/option_tools.py
+++ b/src/libtmux_mcp/tools/option_tools.py
@@ -66,6 +66,7 @@ def show_option(
     scope: t.Literal["server", "session", "window", "pane"] | None = None,
     target: str | None = None,
     global_: bool = False,
+    include_inherited: bool = False,
     socket_name: str | None = None,
 ) -> OptionResult:
     """Show a tmux option value.
@@ -73,6 +74,13 @@ def show_option(
     Use to check tmux configuration values such as history-limit,
     mouse support, or status bar settings.
 
+    **``value: null`` means "not set AT THIS SCOPE", not "not set".**
+    An option inherited from a wider scope reads as null here, so
+    ``show_option("history-limit", scope="session")`` answers null while
+    50000 is in force. Pass ``include_inherited=True`` (tmux's ``-A``)
+    to ask what is actually in effect, which is what most questions —
+    "is mouse mode on?" — really mean.
+
     Parameters
     ----------
     option : str
@@ -85,17 +93,30 @@ def show_option(
         For pane scope: pane ID (e.g. '%1'). Requires scope.
     global_ : bool
         Whether to query the global option.
+    include_inherited : bool
+        Resolve inherited values (tmux ``-A``) so the answer is the
+        value in force at this scope rather than only one set on it.
     socket_name : str, optional
         tmux socket name.
 
     Returns
     -------
     OptionResult
-        Option name and its value.
+        Option name, its value, and the scope that was queried.
     """
     obj, opt_scope = _resolve_option_target(socket_name, scope, target)
-    value = obj.show_option(option, global_=global_, scope=opt_scope)
-    return OptionResult(option=option, value=value)
+    value = obj.show_option(
+        option,
+        global_=global_,
+        scope=opt_scope,
+        include_inherited=include_inherited or None,
+    )
+    return OptionResult(
+        option=option,
+        value=value,
+        scope_queried=scope or ("global" if global_ else "server"),
+        include_inherited=include_inherited,
+    )
 
 
 @handle_tool_errors
diff --git a/src/libtmux_mcp/tools/pane_tools/capture_since.py b/src/libtmux_mcp/tools/pane_tools/capture_since.py
index 7c4314ee..e2dcc9a0 100644
--- a/src/libtmux_mcp/tools/pane_tools/capture_since.py
+++ b/src/libtmux_mcp/tools/pane_tools/capture_since.py
@@ -233,6 +233,15 @@ def _find_unique_cursor_match(
     # ``history_size == history_limit``: measured on tmux 3.7c, a pane
     # with ``history-limit 20`` pins at ``history_size 19``, so an exact
     # comparison never fires on the very panes this guards.
+    #
+    # The cost is a measured false-positive band: on a 50000-line limit
+    # this reports a loss from roughly 92% full, where trim risk is on
+    # but tmux has not evicted yet (clean at 0/10/50/80/88%). Erring
+    # there costs a pessimistic flag and a full visible read, never
+    # silence. Growth in ``history_size`` between cursor and read would
+    # narrow it -- history still growing means nothing was dropped --
+    # but a burst that saturates midway grows AND evicts, so trusting
+    # growth would reopen the silent loss this closes.
     blind = len(fingerprint) == 1 and _history_limit_trim_risk(
         cursor, state, history_limit
     )
diff --git a/src/libtmux_mcp/tools/pane_tools/search.py b/src/libtmux_mcp/tools/pane_tools/search.py
index 1c728a70..2ae6c415 100644
--- a/src/libtmux_mcp/tools/pane_tools/search.py
+++ b/src/libtmux_mcp/tools/pane_tools/search.py
@@ -160,6 +160,18 @@ def search_panes(
         msg = f"Invalid regex pattern: {e}"
         raise ExpectedToolError(msg) from e
 
+    # Reject nonsense pagination rather than answering with an empty
+    # page: ``limit=0`` returned ``matches: []``, which an agent cannot
+    # tell from a genuine miss, and a negative ``offset`` was clamped
+    # to 0 and echoed back unchanged, so the result silently did not
+    # match the request.
+    if offset < 0:
+        msg = f"offset must be zero or greater (received {offset})"
+        raise ExpectedToolError(msg)
+    if limit is not None and limit < 1:
+        msg = f"limit must be at least 1, or null for no limit (received {limit})"
+        raise ExpectedToolError(msg)
+
     server = _get_server(socket_name=socket_name)
 
     uses_scrollback = content_start is not None or content_end is not None
@@ -285,8 +297,8 @@ def search_panes(
     all_matches.sort(key=_pane_id_sort_key)
     total_panes_matched = len(all_matches)
 
-    page_start = max(0, offset)
-    page_end: int | None = None if limit is None else page_start + max(0, limit)
+    page_start = offset
+    page_end: int | None = None if limit is None else page_start + limit
     page_matches = all_matches[page_start:page_end]
 
     skipped_panes = [m.pane_id for m in all_matches[page_start:][len(page_matches) :]]
diff --git a/tests/test_env_tools.py b/tests/test_env_tools.py
index 7e7ed802..bbd09f02 100644
--- a/tests/test_env_tools.py
+++ b/tests/test_env_tools.py
@@ -49,3 +49,27 @@ def test_show_environment_session(mcp_server: Server, mcp_session: Session) -> N
     )
     assert isinstance(result, EnvironmentResult)
     assert isinstance(result.variables, dict)
+
+
+def test_show_environment_separates_removed_from_set(
+    mcp_server: Server, mcp_session: Session
+) -> None:
+    """A removed variable is a name, not a value of ``True``.
+
+    tmux prints a variable marked removed as ``-NAME``. Keeping the
+    dash in the key put the removal in the key -- so a lookup by the
+    real name raised KeyError -- and giving it ``True`` read as "set to
+    true" for a variable that is explicitly unset.
+    """
+    mcp_session.cmd("set-environment", "MCP_ENV_KEPT", "yes")
+    mcp_session.cmd("set-environment", "-r", "MCP_ENV_GONE")
+
+    result = show_environment(
+        session_name=mcp_session.session_name,
+        socket_name=mcp_server.socket_name,
+    )
+
+    assert result.variables["MCP_ENV_KEPT"] == "yes"
+    assert "MCP_ENV_GONE" in result.removed
+    assert "MCP_ENV_GONE" not in result.variables
+    assert not any(name.startswith("-") for name in result.variables)
diff --git a/tests/test_option_tools.py b/tests/test_option_tools.py
index 7b561364..0b6f7bf2 100644
--- a/tests/test_option_tools.py
+++ b/tests/test_option_tools.py
@@ -59,3 +59,37 @@ def test_set_option(mcp_server: Server, mcp_session: Session) -> None:
     )
     assert result.status == "set"
     assert result.option == "display-time"
+
+
+def test_show_option_resolves_inherited_values(
+    mcp_server: Server, mcp_session: Session
+) -> None:
+    """``include_inherited`` answers what is in force, not what is set here.
+
+    A bare session-scope read of an inherited option returns ``None``,
+    which reads as "unset" when the value really is in effect. tmux has
+    ``-A`` for this; it was not exposed, so an agent could ask "is it
+    set at this exact scope?" but never "what is in force?" -- and the
+    latter is what a question like "is mouse mode on?" means.
+    """
+    plain = show_option(
+        option="history-limit",
+        scope="session",
+        target=mcp_session.session_name,
+        socket_name=mcp_server.socket_name,
+    )
+    inherited = show_option(
+        option="history-limit",
+        scope="session",
+        target=mcp_session.session_name,
+        include_inherited=True,
+        socket_name=mcp_server.socket_name,
+    )
+
+    assert plain.value is None
+    assert plain.scope_queried == "session"
+    assert plain.include_inherited is False
+    # The value actually in force is reachable now.
+    assert inherited.value is not None
+    assert int(str(inherited.value)) > 0
+    assert inherited.include_inherited is True
diff --git a/tests/test_pane_tools.py b/tests/test_pane_tools.py
index 6660afc4..5c0e3291 100644
--- a/tests/test_pane_tools.py
+++ b/tests/test_pane_tools.py
@@ -291,6 +291,51 @@ def test_exit_copy_mode_reports_a_pane_that_is_not_in_a_mode(
     exit_copy_mode(pane_id=mcp_pane.pane_id, socket_name=mcp_server.socket_name)
 
 
+class SearchPaginationFixture(t.NamedTuple):
+    """Test fixture for rejected search_panes pagination."""
+
+    test_id: str
+    offset: int
+    limit: int | None
+    error_match: str
+
+
+SEARCH_PAGINATION_FIXTURES: list[SearchPaginationFixture] = [
+    SearchPaginationFixture("negative_offset", -1, None, "offset must be zero"),
+    SearchPaginationFixture("zero_limit", 0, 0, "limit must be at least 1"),
+]
+
+
+@pytest.mark.parametrize(
+    SearchPaginationFixture._fields,
+    SEARCH_PAGINATION_FIXTURES,
+    ids=[fixture.test_id for fixture in SEARCH_PAGINATION_FIXTURES],
+)
+def test_search_panes_rejects_nonsense_pagination(
+    test_id: str,
+    offset: int,
+    limit: int | None,
+    error_match: str,
+    mcp_server: Server,
+) -> None:
+    """Bad pagination errors instead of answering with an empty page.
+
+    ``limit=0`` returned ``matches: []``, which an agent cannot tell
+    from a genuine miss, and a negative ``offset`` was clamped to 0 and
+    echoed back unchanged, so the result silently did not describe the
+    request.
+    """
+    assert test_id
+
+    with pytest.raises(ToolError, match=error_match):
+        search_panes(
+            pattern="anything",
+            offset=offset,
+            limit=limit,
+            socket_name=mcp_server.socket_name,
+        )
+
+
 class DashPayloadArgvFixture(t.NamedTuple):
     """Test fixture for ``--`` placement in the send-keys argv."""
 

From 4568f8a70c54d3fae61b283fffd3faae716dab35 Mon Sep 17 00:00:00 2001
From: Tony Narlock 
Date: Mon, 24 Aug 2026 21:18:37 -0500
Subject: [PATCH 54/71] Mcp(fix[run-command]): Require a shell, and say a
 timeout did not cancel
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

`run_command` assumed a cooperative shell sitting at a prompt, and
neither verified nor documented it. That assumption holds in tests and
breaks in a live session, in two ways.

A full-screen program owns the pane's keyboard, so the exit-status
wrapper is consumed as ITS keystrokes. Measured against `less`:
`s=$?...` became less's save-to-file command, a fragment escaped to a
shell, and the pane was left on less's help screen. In `vi` the same
payload lands in the buffer, where `:`-prefixed fragments are commands
that edit and write files. `alternate_on` was readable before the call
the whole time — `snapshot_pane` already reports it — so the tool now
reads it and refuses, naming the occupant and pointing at `send_keys`.

The guard is deliberately narrow. There is no reliable "pane is busy"
signal: `pane_current_command` is the foreground process, which is
legitimately non-shell for the entire duration of any long command an
agent wants to run, so refusing on that would break the main use.
`alternate_on` means a program has taken the whole grid, which is never
a state where a shell wrapper makes sense.

A timeout does not cancel the command. The keystrokes are already in
the pane's input buffer, so a blocked shell runs them whenever it next
reads a line — verified: a command reporting `timed_out=True` executed
once the blocking `sleep` returned. An agent reading `timed_out` alone
concludes it did not run and retries, which is how a `git push` or a
migration runs twice, the second time unwatched. The result now carries
`command_may_still_run`, and the docstring states both preconditions.

The wrapper is sent through the checked send path, so a rejected send
surfaces as an error rather than as a timeout.

Also: `Invalid buffer name: 'yanked'` misattributed. tmux accepts that
name; it is this server that only touches buffers it allocated, because
tmux buffers can hold OS clipboard history. The message says so, and
states that a copy-mode yank or tmux's own `buffer0` is unreachable by
design rather than leaving that to be inferred.
---
 CHANGES                                | 42 ++++++++++++++++
 src/libtmux_mcp/models.py              | 12 +++++
 src/libtmux_mcp/tools/buffer_tools.py  | 22 +++++++--
 src/libtmux_mcp/tools/pane_tools/io.py | 56 ++++++++++++++++++++-
 tests/test_buffer_tools.py             |  4 +-
 tests/test_pane_tools.py               | 68 ++++++++++++++++++++++++++
 6 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/CHANGES b/CHANGES
index 179af9d6..ade5d2c4 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,48 @@ _Notes on upcoming releases will be added here_
 
 ### What's new
 
+#### A non-MCP buffer name is described accurately
+
+`load_buffer`/`paste_buffer`/`show_buffer` answered a name like
+`yanked` or tmux's own `buffer0` with `Invalid buffer name` — but those
+names are perfectly valid to tmux. What is true is that they were not
+allocated by this server, which only touches buffers it created because
+tmux buffers can hold OS clipboard history.
+
+The message now says that, and the suggestion states the consequence
+plainly: a buffer created outside this server — a copy-mode yank, or
+`buffer0` — is not reachable by design, and `load_buffer` is the way to
+stage content it can read back.
+
+#### `run_command` checks the pane has a shell to talk to
+
+`run_command` assumed a cooperative shell sitting at a prompt, and
+neither verified nor documented it. The assumption holds in tests and
+breaks in a live session, in two ways.
+
+**A full-screen program owns the keyboard.** Sent to a pane running
+`less`, this tool's exit-status wrapper was consumed as *less's*
+keystrokes — `s=$?` became its save-to-file command and a fragment
+escaped to a shell, leaving `Cannot write to "=$?; tmux ...` on screen.
+In `vi` the same payload lands in the buffer, where `:`-prefixed
+fragments are commands that edit and write files. `run_command` now
+reads `alternate_on` before sending and refuses, naming the program and
+pointing at `send_keys` for raw input. The signal was already there:
+`snapshot_pane` reports both `alternate_on` and `pane_current_command`.
+
+**A timed-out command is sent, not cancelled.** With the pane blocked on
+a foreground command, the keystrokes sit in the terminal's input buffer
+and the shell runs them whenever it next reads a line — verified: a
+command that reported `timed_out=True` executed once the blocking
+`sleep` returned, with no bound on when. An agent reading `timed_out`
+alone concludes the command did not run and retries, which is how a
+`git push`, a migration or a deploy runs twice, the second time when
+nothing is watching. The result now carries `command_may_still_run`,
+which says so.
+
+The wrapper is also sent through the checked send path, so a rejected
+send surfaces as an error rather than as a timeout.
+
 #### `show_option` can answer what is actually in force
 
 `show_option(option="history-limit", scope="session")` returned
diff --git a/src/libtmux_mcp/models.py b/src/libtmux_mcp/models.py
index 0b9f9d81..be1cb6de 100644
--- a/src/libtmux_mcp/models.py
+++ b/src/libtmux_mcp/models.py
@@ -416,6 +416,18 @@ class RunCommandResult(BaseModel):
         description="Shell exit status, or None when the command timed out",
     )
     timed_out: bool = Field(description="True when the wait timed out")
+    command_may_still_run: bool = Field(
+        default=False,
+        description=(
+            "True when the wait timed out, meaning the command was SENT "
+            "but not observed to finish. It is not cancelled: the "
+            "keystrokes sit in the pane's input buffer and the shell "
+            "runs them whenever it next reads a line, which may be long "
+            "after this call returned. Do NOT retry a non-idempotent "
+            "command on this result -- that is how a `git push` or a "
+            "migration runs twice. Check the pane first."
+        ),
+    )
     elapsed_seconds: float = Field(description="Time spent waiting in seconds")
     output: list[str] = Field(
         default_factory=list,
diff --git a/src/libtmux_mcp/tools/buffer_tools.py b/src/libtmux_mcp/tools/buffer_tools.py
index 6663d7e7..1fb23bb3 100644
--- a/src/libtmux_mcp/tools/buffer_tools.py
+++ b/src/libtmux_mcp/tools/buffer_tools.py
@@ -125,15 +125,29 @@ def _validate_buffer_name(name: str) -> str:
     >>> _validate_buffer_name("clipboard")
     Traceback (most recent call last):
     ...
-    libtmux_mcp._utils.ExpectedToolError: Invalid buffer name: 'clipboard'
+    libtmux_mcp._utils.ExpectedToolError: 'clipboard' is not an MCP-allocated buffer
     >>> _validate_buffer_name("libtmux_mcp_shortuuid_buf")
     Traceback (most recent call last):
     ...
-    libtmux_mcp._utils.ExpectedToolError: Invalid buffer name: 'libtmux_mcp_...'
+    libtmux_mcp._utils.ExpectedToolError: 'libtmux_mcp_...' is not an MCP-...
     """
     if not _BUFFER_NAME_RE.fullmatch(name):
-        msg = f"Invalid buffer name: {name!r}"
-        raise ExpectedToolError(msg)
+        # Not "invalid": tmux accepts any of these names happily. It is
+        # this server that only touches buffers it allocated, because
+        # tmux buffers can hold OS clipboard history and a tool that
+        # reads arbitrary ones is a clipboard reader.
+        msg = f"{name!r} is not an MCP-allocated buffer"
+        raise ExpectedToolError(
+            msg,
+            suggestion=(
+                "This server only reads and writes buffers it created "
+                "(libtmux_mcp_<32-hex>_