From fb2029e50a4b97e53d7502bc63ff6cf5ef1d6398 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 9 Aug 2026 07:45:46 -0500 Subject: [PATCH 01/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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 a348a012e730dc7e293f53397eca8aeb08ff56c4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 15 Aug 2026 05:43:22 -0500 Subject: [PATCH 33/36] pane_tools(refactor[capture_since]): Read the cursor from libtmux why: The cursor machinery behind this tool is general-purpose tmux observation, not an MCP concern, and libtmux now ships it as Pane.capture_since(). Keeping a second copy here means two implementations of the same anchor arithmetic drifting apart. what: - Call Pane.capture_since() instead of the local read driver; drop the cursor codec, anchor-loss and trim-risk checks, fingerprint re-anchoring, and the stable double-read - Keep max_lines/max_bytes truncation, which bounds an agent response and is not a tmux concern - Map libtmux's CaptureCursorError family to an agent-facing error advising a fresh cursor, ahead of the generic tmux-error catch-all - Drop state.py readers that only this tool used; wait.py keeps the formats and parser it issues through its own bounded subprocess - Pin libtmux to the branch adding capture_since, temporarily The cursor wire format is unchanged, so the tool's tests pass without modification and previously issued cursors still decode. libtmux PR: https://github.com/tmux-python/libtmux/pull/741 --- CHANGES | 16 + pyproject.toml | 6 + src/libtmux_mcp/_utils.py | 11 + .../tools/pane_tools/capture_since.py | 379 +----------------- src/libtmux_mcp/tools/pane_tools/state.py | 39 +- tests/test_pane_tools.py | 12 +- uv.lock | 8 +- 7 files changed, 68 insertions(+), 403 deletions(-) diff --git a/CHANGES b/CHANGES index b40708cb..3dd475a0 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,22 @@ _Notes on upcoming releases will be added here_ +### Development + +#### `capture_since` reads its cursor from libtmux + +The cursor machinery behind the `capture_since` tool — anchor arithmetic, +history-trim re-anchoring, the stable double-read, and the serialized cursor +format — now comes from libtmux's `Pane.capture_since()` instead of living +here. The tool keeps the part that is an MCP concern: bounding a response with +`max_lines` and `max_bytes` so one observation cannot blow an agent's context +window. + +The cursor wire format is unchanged, so cursors issued by earlier versions +still decode. A cursor that no longer describes its pane now raises libtmux's +`CaptureCursorError` family, which maps to an agent-facing error advising a +fresh cursor rather than the generic tmux-error wording. + ### Documentation #### opencode joins the install picker diff --git a/pyproject.toml b/pyproject.toml index 036f261a..10aff8e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,12 @@ lint = [ requires = ["hatchling"] build-backend = "hatchling.build" +[tool.uv.sources] +# TEMPORARY: tracks the libtmux branch that adds Pane.capture_since(). +# Drop this block and the matching uv.lock entry once that lands in a +# released libtmux. https://github.com/tmux-python/libtmux/pull/741 +libtmux = { git = "https://github.com/tmux-python/libtmux.git", branch = "capture-since" } + [tool.uv.exclude-newer-package] # git-pull packages release in lockstep with their workspaces, so a # fresh release blocking on the 3-day cooldown blocks every diff --git a/src/libtmux_mcp/_utils.py b/src/libtmux_mcp/_utils.py index 587e2d85..1b13a98f 100644 --- a/src/libtmux_mcp/_utils.py +++ b/src/libtmux_mcp/_utils.py @@ -1069,6 +1069,17 @@ def _map_exception_to_tool_error(fn_name: str, e: BaseException) -> ToolError: f"Pane not found: {e}", suggestion="Call list_panes to discover valid pane ids.", ) + if isinstance(e, exc.CaptureCursorError): + # Not a tmux failure — the cursor the agent replayed no longer + # describes the pane it was taken from, so the generic "tmux + # error:" prefix would point at the wrong thing to fix. + return ExpectedToolError( + str(e), + suggestion=( + "Call capture_since without a cursor to start a fresh " + "observation of this pane." + ), + ) if isinstance(e, exc.LibTmuxException): return ExpectedToolError(f"tmux error: {e}") logger.exception("unexpected error in MCP tool %s", fn_name) diff --git a/src/libtmux_mcp/tools/pane_tools/capture_since.py b/src/libtmux_mcp/tools/pane_tools/capture_since.py index 5d6f77de..dddac45e 100644 --- a/src/libtmux_mcp/tools/pane_tools/capture_since.py +++ b/src/libtmux_mcp/tools/pane_tools/capture_since.py @@ -1,16 +1,20 @@ -"""Incremental capture tool for tmux pane observation.""" +"""Incremental capture tool for tmux pane observation. + +The cursor machinery — anchor arithmetic, trim-risk re-anchoring, the +stable double-read, and the serialized cursor format — lives in libtmux +as :meth:`~libtmux.pane.Pane.capture_since`. What remains here is the +part that is an MCP concern rather than a tmux one: bounding the +response so a single observation cannot blow an agent's context window. +""" from __future__ import annotations import asyncio -import base64 -import binascii -import hashlib -import json import time -import typing as t from dataclasses import dataclass +from libtmux.capture import CaptureCursor + from libtmux_mcp._utils import ( ExpectedToolError, _get_server, @@ -19,47 +23,10 @@ ) from libtmux_mcp.models import CaptureSinceResult from libtmux_mcp.tools.pane_tools.io import CAPTURE_DEFAULT_MAX_LINES -from libtmux_mcp.tools.pane_tools.state import ( - _PaneState, - _raise_if_pane_lifecycle_changed, - _read_history_limit, - _read_pane_state, -) - -if t.TYPE_CHECKING: - from libtmux.pane import Pane - CAPTURE_SINCE_DEFAULT_MAX_LINES = CAPTURE_DEFAULT_MAX_LINES CAPTURE_SINCE_DEFAULT_MAX_BYTES = 128_000 -_CURSOR_PREFIX = "capture-since-v1:" -_CURSOR_VERSION = 1 -_STABLE_READ_ATTEMPTS = 3 - - -@dataclass(frozen=True) -class _CaptureCursor: - """Decoded capture_since cursor payload.""" - - pane_id: str - pane_pid: str - history_size: int - pane_height: int - anchor_abs: int - anchor_hash: str | None - below_hashes: tuple[str, ...] - - -@dataclass(frozen=True) -class _PaneRead: - """Synchronous tmux read result used by the async tool wrapper.""" - - state: _PaneState - cursor_rows: list[str] - lines: list[str] - lines_missed: bool - @dataclass(frozen=True) class _LimitedLines: @@ -71,305 +38,6 @@ class _LimitedLines: truncated_bytes: int -def _line_hash(line: str) -> str: - """Return a stable content hash for a tmux row.""" - return hashlib.sha256(line.encode("utf-8", "surrogateescape")).hexdigest() - - -def _capture_rows( - pane: Pane, - *, - start: t.Literal["-"] | int | None = None, - end: t.Literal["-"] | int | None = None, -) -> list[str]: - """Return pane rows as a concrete list.""" - rows = pane.capture_pane(start=start, end=end) - if rows is None: - return [] - return list(rows) - - -def _capture_cursor_rows(pane: Pane, state: _PaneState) -> list[str]: - """Capture rows from the cursor through the visible bottom.""" - if state.cursor_y >= state.pane_height: - return [] - return _capture_rows(pane, start=state.cursor_y, end=None) - - -def _same_state(left: _PaneState, right: _PaneState) -> bool: - """Return True when two pane snapshots describe the same grid point.""" - return left == right - - -def _raise_if_dead_without_baseline(pane: Pane, state: _PaneState) -> None: - """Raise a tool error for a dead pane before a cursor exists.""" - if state.pane_dead: - msg = f"pane {pane.pane_id} died during pane read" - raise ExpectedToolError(msg) - - -def _read_stable_visible( - pane: Pane, - *, - baseline_pid: str | None = None, -) -> _PaneRead: - """Capture the visible pane and cursor rows with a stable state snapshot.""" - for _attempt in range(_STABLE_READ_ATTEMPTS): - before = _read_pane_state(pane) - if baseline_pid is None: - _raise_if_dead_without_baseline(pane, before) - expected_pid = before.pane_pid - else: - expected_pid = baseline_pid - _raise_if_pane_lifecycle_changed(pane.pane_id, before, expected_pid) - - lines = _capture_rows(pane) - cursor_rows = _capture_cursor_rows(pane, before) - after = _read_pane_state(pane) - _raise_if_pane_lifecycle_changed(pane.pane_id, after, expected_pid) - if _same_state(before, after): - return _PaneRead( - state=after, - cursor_rows=cursor_rows, - lines=lines, - lines_missed=False, - ) - - state = _read_pane_state(pane) - if baseline_pid is None: - _raise_if_dead_without_baseline(pane, state) - else: - _raise_if_pane_lifecycle_changed(pane.pane_id, state, baseline_pid) - return _PaneRead( - state=state, - cursor_rows=_capture_cursor_rows(pane, state), - lines=_capture_rows(pane), - lines_missed=True, - ) - - -def _cursor_anchor_lost(cursor: _CaptureCursor, state: _PaneState) -> bool: - """Return True when sampled state proves tmux lost the cursor anchor.""" - bottom_abs = state.history_size + state.pane_height - 1 - if cursor.anchor_abs > bottom_abs: - return True - # A complete history wipe (``clear-history``) always destroys the - # anchor regardless of pane height — the grid is reset to zero. - if state.history_size == 0 and cursor.history_size > 0: - return True - # ``anchor_abs < history_size`` means the anchor has scrolled into - # retained history, where ``capture-pane -S`` can still address it - # with a negative start offset. - # - # The ``pane_height`` guard distinguishes resize-grow (which pulls - # rows from history back into the visible region without freeing - # data) from actual trim (where row data is destroyed). - return state.history_size < cursor.history_size and ( - state.pane_height <= cursor.pane_height - ) - - -def _history_limit_trim_risk( - cursor: _CaptureCursor, - state: _PaneState, - history_limit: int, -) -> bool: - """Return True when tmux may have rebased retained-history rows.""" - if history_limit <= 0: - return True - trim_batch = max(history_limit // 10, 1) - risk_floor = history_limit - trim_batch - 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.""" - if cursor.anchor_hash is None: - return None - - fingerprint = (cursor.anchor_hash, *cursor.below_hashes) - if len(rows) < len(fingerprint): - return None - - match_index: int | None = None - for index in range(len(rows) - len(fingerprint) + 1): - candidate = rows[index : index + len(fingerprint)] - candidate_hashes = tuple(_line_hash(line) for line in candidate) - if candidate_hashes != fingerprint: - continue - if match_index is not None: - return None - match_index = index - return match_index - - -def _drop_previously_seen_rows( - rows: list[str], - cursor: _CaptureCursor, -) -> list[str]: - """Drop the cursor anchor and below-cursor rows already represented.""" - if not rows: - return [] - - output: list[str] = [] - tail = rows - if cursor.anchor_hash is not None and _line_hash(rows[0]) == cursor.anchor_hash: - tail = rows[1:] - else: - output.append(rows[0]) - tail = rows[1:] - - drop = 0 - for expected_hash, line in zip(cursor.below_hashes, tail, strict=False): - if _line_hash(line) != expected_hash: - break - drop += 1 - output.extend(tail[drop:]) - return output - - -def _read_delta(pane: Pane, cursor: _CaptureCursor) -> _PaneRead: - """Capture rows since ``cursor`` or fall back to visible content on loss.""" - history_limit = _read_history_limit(pane) - for _attempt in range(_STABLE_READ_ATTEMPTS): - before = _read_pane_state(pane) - _raise_if_pane_lifecycle_changed(pane.pane_id, before, cursor.pane_pid) - if _cursor_anchor_lost(cursor, before): - missed = _read_stable_visible(pane, baseline_pid=cursor.pane_pid) - return _PaneRead( - state=missed.state, - cursor_rows=missed.cursor_rows, - lines=missed.lines, - lines_missed=True, - ) - - trim_risk = _history_limit_trim_risk(cursor, before, history_limit) - start = cursor.anchor_abs - before.history_size - rows = ( - _capture_rows(pane, start="-", end=None) - if trim_risk - else ( - [] - if start >= before.pane_height - else _capture_rows(pane, start=start, end=None) - ) - ) - cursor_rows = _capture_cursor_rows(pane, before) - after = _read_pane_state(pane) - _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) - if match_index is None: - missed = _read_stable_visible(pane, baseline_pid=cursor.pane_pid) - return _PaneRead( - state=missed.state, - cursor_rows=missed.cursor_rows, - lines=missed.lines, - lines_missed=True, - ) - rows = rows[match_index:] - return _PaneRead( - state=after, - cursor_rows=cursor_rows, - lines=_drop_previously_seen_rows(rows, cursor), - lines_missed=False, - ) - - missed = _read_stable_visible(pane, baseline_pid=cursor.pane_pid) - return _PaneRead( - state=missed.state, - cursor_rows=missed.cursor_rows, - lines=missed.lines, - lines_missed=True, - ) - - -def _build_cursor(pane_id: str, state: _PaneState, cursor_rows: list[str]) -> str: - """Encode the current cursor anchor as an opaque string.""" - payload: dict[str, t.Any] = { - "version": _CURSOR_VERSION, - "pane_id": pane_id, - "pane_pid": state.pane_pid, - "history_size": state.history_size, - "pane_height": state.pane_height, - "anchor_abs": state.history_size + state.cursor_y, - "anchor_hash": _line_hash(cursor_rows[0]) if cursor_rows else None, - "below_hashes": [_line_hash(line) for line in cursor_rows[1:]], - } - raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode() - encoded = base64.urlsafe_b64encode(raw).decode().rstrip("=") - return f"{_CURSOR_PREFIX}{encoded}" - - -def _raise_invalid_cursor(reason: str) -> t.NoReturn: - """Raise a consistently worded invalid-cursor error.""" - msg = f"invalid capture_since cursor: {reason}" - raise ExpectedToolError(msg) - - -def _cursor_str(payload: t.Mapping[str, t.Any], key: str) -> str: - """Read a required string from a cursor payload.""" - value = payload.get(key) - if not isinstance(value, str) or not value: - reason = f"missing or invalid {key}" - _raise_invalid_cursor(reason) - return value - - -def _cursor_int(payload: t.Mapping[str, t.Any], key: str) -> int: - """Read a required non-negative integer from a cursor payload.""" - value = payload.get(key) - if not isinstance(value, int) or isinstance(value, bool) or value < 0: - reason = f"missing or invalid {key}" - _raise_invalid_cursor(reason) - return value - - -def _decode_cursor(cursor: str) -> _CaptureCursor: - """Decode and validate an opaque ``capture_since`` cursor.""" - if not cursor.startswith(_CURSOR_PREFIX): - reason = "unsupported cursor format" - _raise_invalid_cursor(reason) - encoded = cursor.removeprefix(_CURSOR_PREFIX) - padding = "=" * (-len(encoded) % 4) - try: - raw = base64.urlsafe_b64decode(f"{encoded}{padding}") - payload: t.Any = json.loads(raw) - except (binascii.Error, json.JSONDecodeError, UnicodeDecodeError) as err: - reason = "could not decode payload" - msg = f"invalid capture_since cursor: {reason}" - raise ExpectedToolError(msg) from err - - if not isinstance(payload, dict): - reason = "payload is not an object" - _raise_invalid_cursor(reason) - if payload.get("version") != _CURSOR_VERSION: - reason = "unsupported cursor version" - _raise_invalid_cursor(reason) - - anchor_hash_value = payload.get("anchor_hash") - if anchor_hash_value is not None and not isinstance(anchor_hash_value, str): - reason = "missing or invalid anchor_hash" - _raise_invalid_cursor(reason) - below_hashes_value = payload.get("below_hashes") - if not isinstance(below_hashes_value, list) or not all( - isinstance(item, str) for item in below_hashes_value - ): - reason = "missing or invalid below_hashes" - _raise_invalid_cursor(reason) - - return _CaptureCursor( - pane_id=_cursor_str(payload, "pane_id"), - pane_pid=_cursor_str(payload, "pane_pid"), - history_size=_cursor_int(payload, "history_size"), - pane_height=_cursor_int(payload, "pane_height"), - anchor_abs=_cursor_int(payload, "anchor_abs"), - anchor_hash=anchor_hash_value, - below_hashes=tuple(below_hashes_value), - ) - - def _validate_limits(max_lines: int | None, max_bytes: int | None) -> None: """Validate caller-supplied truncation limits.""" if max_lines is not None and max_lines <= 0: @@ -391,7 +59,13 @@ def _limit_lines( max_lines: int | None, max_bytes: int | None, ) -> _LimitedLines: - """Apply tail-preserving line and byte limits.""" + """Apply tail-preserving line and byte limits. + + Runs after the capture completes and never feeds back into the + cursor, which libtmux builds from pane state rather than from these + rows. Truncating a response therefore cannot shift where the next + observation resumes. + """ kept = list(lines) truncated_lines = 0 truncated_bytes = 0 @@ -485,7 +159,7 @@ async def capture_since( metadata. """ _validate_limits(max_lines, max_bytes) - decoded = _decode_cursor(cursor) if cursor is not None else None + decoded = CaptureCursor.from_str(cursor) if cursor is not None else None if decoded is not None and not any( value is not None for value in (pane_id, session_name, session_id, window_id) ): @@ -501,24 +175,15 @@ async def capture_since( ) assert pane.pane_id is not None - if decoded is not None and pane.pane_id != decoded.pane_id: - msg = ( - f"cursor pane {decoded.pane_id} does not match requested pane " - f"{pane.pane_id}" - ) - raise ExpectedToolError(msg) - start_time = time.monotonic() - if decoded is None: - read = await asyncio.to_thread(_read_stable_visible, pane) - else: - read = await asyncio.to_thread(_read_delta, pane, decoded) - + # Off the event loop: every tmux round-trip inside capture_since is a + # blocking subprocess call, and a stable read makes several. + read = await asyncio.to_thread(pane.capture_since, decoded) limited = _limit_lines(read.lines, max_lines=max_lines, max_bytes=max_bytes) elapsed = time.monotonic() - start_time return CaptureSinceResult( pane_id=pane.pane_id, - cursor=_build_cursor(pane.pane_id, read.state, read.cursor_rows), + cursor=str(read.cursor), lines=limited.lines, elapsed_seconds=round(elapsed, 3), lines_missed=read.lines_missed, diff --git a/src/libtmux_mcp/tools/pane_tools/state.py b/src/libtmux_mcp/tools/pane_tools/state.py index d0220aeb..3f68607a 100644 --- a/src/libtmux_mcp/tools/pane_tools/state.py +++ b/src/libtmux_mcp/tools/pane_tools/state.py @@ -6,9 +6,6 @@ from libtmux_mcp._utils import ExpectedToolError -if t.TYPE_CHECKING: - from libtmux.pane import Pane - class _PaneState(t.NamedTuple): """Per-read snapshot of tmux pane grid and lifecycle state. @@ -18,7 +15,7 @@ class _PaneState(t.NamedTuple): ``history_size + cursor_y`` gives the absolute tmux grid row of the current cursor. - Wire format parsed by :func:`_read_pane_state`:: + Wire format parsed by :func:`_parse_pane_state`:: #{history_size}|#{cursor_y}|#{pane_height}|#{pane_pid}|#{pane_dead} |#{alternate_on} @@ -45,9 +42,9 @@ class _PaneState(t.NamedTuple): alternate_on: bool = False -#: tmux format string read by :func:`_read_pane_state`. Exposed as a -#: constant because the wait tools re-issue the identical read through -#: a timeout-bounded ``subprocess.run`` rather than libtmux (whose +#: tmux format string whose single output line :func:`_parse_pane_state` +#: decodes. Exposed as a constant because the wait tools issue this read +#: through a timeout-bounded ``subprocess.run`` rather than libtmux (whose #: ``Popen.communicate()`` has no timeout and can wedge a worker #: thread). It is a fixed literal — no caller-supplied text is ever #: interpolated into a tmux format string, because tmux's format @@ -83,19 +80,6 @@ def _parse_pane_state(raw: str) -> _PaneState: ) -def _read_pane_state(pane: Pane) -> _PaneState: - """Return a :class:`_PaneState` snapshot for ``pane``. - - Combines the tmux state reads needed by wait and incremental - capture tools into a single ``display-message`` call. ``pane_pid`` - and ``pane_dead`` surface respawn-pane and pane-death events that - invalidate cursor or baseline anchors. - """ - stdout = pane.display_message(PANE_STATE_FORMAT, get_text=True) - raw = stdout[0] if stdout else "0|0|0||0" - return _parse_pane_state(raw) - - def _raise_if_pane_lifecycle_changed( pane_id: str | None, state: _PaneState, baseline_pid: str ) -> None: @@ -116,18 +100,3 @@ def _raise_if_pane_lifecycle_changed( "cursor/baseline anchor is no longer valid" ) raise ExpectedToolError(msg) - - -def _read_history_limit(pane: Pane) -> int: - """Read the pane's ``history-limit`` once. - - Fixed at pane creation — a retroactive ``set-option history-limit`` - only takes effect in tmux 3.7+ (commit ``e7b1575``); older versions - require a new pane. Safe to cache for the lifetime of a single - wait or capture operation. Kept separate from :func:`_read_pane_state` - so per-tick reads do not pay for a value that never changes between - ticks. - """ - stdout = pane.display_message(HISTORY_LIMIT_FORMAT, get_text=True) - raw = stdout[0] if stdout else "0" - return int(raw) diff --git a/tests/test_pane_tools.py b/tests/test_pane_tools.py index 1e3d2ff4..48c2b2bf 100644 --- a/tests/test_pane_tools.py +++ b/tests/test_pane_tools.py @@ -1351,8 +1351,8 @@ def test_capture_since_marks_lines_missed_after_history_limit_trim( Floods past ``history-limit`` then clears history to guarantee the cursor anchor is destroyed. 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. + tmux 3.6 retains enough of the original prompt that libtmux's + fingerprint search re-anchors on the surviving hash. """ import asyncio @@ -1561,9 +1561,11 @@ def test_capture_since_marks_lines_missed_after_clear_history_with_resize( ) -> None: """clear-history + pane resize still detects anchor loss. - Regression: ``_cursor_anchor_lost`` used a ``pane_height`` guard - that returned False when the pane grew after ``clear-history``, - masking the complete history wipe. + Regression: libtmux's anchor-loss check uses a ``pane_height`` guard + to tell a resize-grow from a real trim, and an early version of it + returned False when the pane grew after ``clear-history``, masking + the complete history wipe. Kept here because this tool is what + surfaces that loss to an agent. """ import asyncio diff --git a/uv.lock b/uv.lock index f9912b58..ca7a707d 100644 --- a/uv.lock +++ b/uv.lock @@ -1324,11 +1324,7 @@ wheels = [ [[package]] name = "libtmux" version = "0.62.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/b8/0410c8487f7673926d141a5aaaf60133984a564b3c90eb4f3f99e92e7740/libtmux-0.62.0.tar.gz", hash = "sha256:41e9e80602b2656fd119b13253b27bad46af5cfef32099be53cdd391e2936b61", size = 571757, upload-time = "2026-07-12T21:48:02.781Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/96/471ac01844ee157fe794446e17568e9bf219d323575c2acc7957a9a2d8c9/libtmux-0.62.0-py3-none-any.whl", hash = "sha256:626aa6fae45e3a423e1acc81604efafa63b7262b1d67b2c7a8778b1ad691456b", size = 127700, upload-time = "2026-07-12T21:48:01.52Z" }, -] +source = { git = "https://github.com/tmux-python/libtmux.git?branch=capture-since#ee6dcb75ccb096d5c54fcc6281804863e0a8fc7f" } [[package]] name = "libtmux-mcp" @@ -1391,7 +1387,7 @@ testing = [ [package.metadata] requires-dist = [ { name = "fastmcp", specifier = ">=3.4.2,<4.0.0" }, - { name = "libtmux", specifier = ">=0.62.0,<1.0" }, + { name = "libtmux", git = "https://github.com/tmux-python/libtmux.git?branch=capture-since" }, ] [package.metadata.requires-dev] From 80c0f6a5b403ad21c6b49c4a3f203f0ab9c67c09 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 15 Aug 2026 06:19:14 -0500 Subject: [PATCH 34/36] middleware(fix[retry]): Stop retrying capture cursor failures why: Moving the cursor to libtmux changed these from bare ExpectedToolError into chained LibTmuxException subclasses, and the retry middleware decides by walking __cause__. A malformed, cross-pane, dead-pane, or respawned-pane cursor therefore started costing a backoff window and a second tmux round-trip before failing identically. what: - List CaptureCursorError in NON_RETRYABLE_EXCEPTIONS, covering both InvalidCaptureCursor and PaneLifecycleChanged - Extend the deterministic-failure parametrization to both, verified to fail without the entry - Re-pin libtmux to the branch tip --- src/libtmux_mcp/middleware.py | 4 ++++ tests/test_middleware.py | 9 ++++++--- uv.lock | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libtmux_mcp/middleware.py b/src/libtmux_mcp/middleware.py index 9226bf2a..d60fafae 100644 --- a/src/libtmux_mcp/middleware.py +++ b/src/libtmux_mcp/middleware.py @@ -692,6 +692,10 @@ async def on_call_tool( libtmux_exc.ObjectDoesNotExist, libtmux_exc.MultipleObjectsReturned, libtmux_exc.PaneNotFound, + # Covers InvalidCaptureCursor and PaneLifecycleChanged. A cursor that + # does not describe its pane describes it no better on a second look, + # and a respawned or dead pane does not un-respawn. + libtmux_exc.CaptureCursorError, libtmux_exc.NoWindowsExist, libtmux_exc.BadSessionName, libtmux_exc.TmuxSessionExists, diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 495e1532..b39fecbe 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -957,6 +957,8 @@ async def real_call_next(_context: t.Any) -> t.Any: libtmux_exc.NoWindowsExist, libtmux_exc.BadSessionName(reason="contains periods", session_name="a.b"), libtmux_exc.TmuxSessionExists("session exists"), + libtmux_exc.InvalidCaptureCursor("invalid capture_since cursor"), + libtmux_exc.PaneLifecycleChanged("pane %99 was respawned"), ], ids=lambda e: type(e).__name__ if isinstance(e, Exception) else e.__name__, ) @@ -966,9 +968,10 @@ def test_readonly_retry_skips_deterministic_failures(raised: Exception) -> None: Every one of these descends from ``LibTmuxException``, which is the retry trigger — so without :data:`NON_RETRYABLE_EXCEPTIONS` they would all be retried. None of them can succeed on the second look: a pane that is not - there will not appear during a backoff window, and an ambiguous match does - not become unambiguous. Retrying buys a second tmux round-trip and 100 ms - of latency in order to fail identically. + there will not appear during a backoff window, an ambiguous match does + not become unambiguous, and a capture cursor that does not describe its + pane will not start describing it. Retrying buys a second tmux round-trip + and 100 ms of latency in order to fail identically. """ middleware = ReadonlyRetryMiddleware(max_retries=1, base_delay=0.0) ctx = _retry_context(tags={TAG_READONLY}) diff --git a/uv.lock b/uv.lock index ca7a707d..159ccd22 100644 --- a/uv.lock +++ b/uv.lock @@ -1324,7 +1324,7 @@ wheels = [ [[package]] name = "libtmux" version = "0.62.0" -source = { git = "https://github.com/tmux-python/libtmux.git?branch=capture-since#ee6dcb75ccb096d5c54fcc6281804863e0a8fc7f" } +source = { git = "https://github.com/tmux-python/libtmux.git?branch=capture-since#aafd8cc59f215ce57949c0a5e2e9f4ec54398610" } [[package]] name = "libtmux-mcp" From 68fc2f2feeca813cc89f13e6db4c5c9062840064 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 15 Aug 2026 06:49:59 -0500 Subject: [PATCH 35/36] py(deps) Re-pin libtmux to the capture-since tip why: Keep CI resolving the branch commit the tool is developed against. --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 159ccd22..53eb75c2 100644 --- a/uv.lock +++ b/uv.lock @@ -1324,7 +1324,7 @@ wheels = [ [[package]] name = "libtmux" version = "0.62.0" -source = { git = "https://github.com/tmux-python/libtmux.git?branch=capture-since#aafd8cc59f215ce57949c0a5e2e9f4ec54398610" } +source = { git = "https://github.com/tmux-python/libtmux.git?branch=capture-since#9c0137a873004230db13ba5355ba9d9ab01418ac" } [[package]] name = "libtmux-mcp" From 6e3fe545fb356947cbb6601ad1f22f9a9a596483 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 15 Aug 2026 07:58:28 -0500 Subject: [PATCH 36/36] tests(fix[capture_since]): Slow the tmux chokepoint, not one wrapper why: The off-loop test injected delay by patching Pane.capture_pane. libtmux's capture_since now issues capture-pane through Pane.cmd directly, so the patch stopped intercepting anything and the test measured an instant call rather than a blocking one. what: - Slow Pane.cmd, which every tmux round-trip in a capture passes through, so the delay cannot be bypassed by a wrapper change - Re-pin libtmux to the branch tip --- tests/test_pane_tools.py | 19 ++++++++++++++----- uv.lock | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/test_pane_tools.py b/tests/test_pane_tools.py index 48c2b2bf..f131a1ed 100644 --- a/tests/test_pane_tools.py +++ b/tests/test_pane_tools.py @@ -1658,17 +1658,26 @@ def _is_dead() -> bool: def test_capture_since_does_not_block_event_loop( mcp_server: Server, mcp_pane: Pane, monkeypatch: pytest.MonkeyPatch ) -> None: - """``capture_since`` runs blocking tmux captures off the event loop.""" + """``capture_since`` runs blocking tmux captures off the event loop. + + Slows ``Pane.cmd``, the one chokepoint every tmux round-trip in a + capture passes through, rather than a single wrapper method. Patching + a specific wrapper would silently stop injecting delay if libtmux + changed which wrapper the read is built on, and the test would then + pass without exercising anything. + """ import asyncio import time as _time from libtmux.pane import Pane as _LibtmuxPane - def _slow_capture(self: _LibtmuxPane, *_a: object, **_kw: object) -> list[str]: - _time.sleep(0.15) - return [] + real_cmd = _LibtmuxPane.cmd + + def _slow_cmd(self: _LibtmuxPane, *args: str) -> t.Any: + _time.sleep(0.05) + return real_cmd(self, *args) - monkeypatch.setattr(_LibtmuxPane, "capture_pane", _slow_capture) + monkeypatch.setattr(_LibtmuxPane, "cmd", _slow_cmd) async def _drive() -> int: ticks = 0 diff --git a/uv.lock b/uv.lock index 53eb75c2..973f0ecd 100644 --- a/uv.lock +++ b/uv.lock @@ -1324,7 +1324,7 @@ wheels = [ [[package]] name = "libtmux" version = "0.62.0" -source = { git = "https://github.com/tmux-python/libtmux.git?branch=capture-since#9c0137a873004230db13ba5355ba9d9ab01418ac" } +source = { git = "https://github.com/tmux-python/libtmux.git?branch=capture-since#447a1624d86926fafce3e10f5d4c5d4c3b5a6f1d" } [[package]] name = "libtmux-mcp"