diff --git a/CHANGELOG.md b/CHANGELOG.md index fda57f9..18da5ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,14 @@ `CliError`; a missing or old Node raises `NodeLocatorError`. - `verify` drops a bundle's top-level `trustRegistry` before the CLI sees it (typedstandards#136). - `CLI_VERSION = "0.2.0"` and `cli_version()`. +- `pin(url)`: fetches once and returns the bytes with a `queries[]` retrieval entry (`url`, + `sha256`, `bytes`, `httpStatus`, `fetchedAt`; `rowsUpdatedAt` and `datasetId` for a portal + resource); `save=` writes both. +- `badge_cell`: the verifier badge as a notebook's first cell, or a `mo.md` cell's source; no hash + and no time in the cell. +- `comparison_cell`: the spec §8.7.4 comparison cell, appended as the last cell; values must be + literals. +- `sidecar`: `.record.yaml` from `view`'s output, without `package` and + `trustRegistry`. +- `show`: HTML for Jupyter (`_repr_html_`) or Marimo (`mo.Html`) from a record and its + `verify --json` result. diff --git a/CLAUDE.md b/CLAUDE.md index 2ef5aa8..ed9f935 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,10 @@ manager a non-interactive shell may have no `node` on `PATH`; load it first `uv sync --reinstall-package typedstandards`. - Another Python: `uv run --python 3.11 pytest`. Another Node: put it first on `PATH`, or set `TYPEDSTANDARDS_NODE`. +- Every test runs offline: an autouse fixture in `tests/conftest.py` makes socket connect, bind + and `create_connection` raise. `pin`'s tests use `httpx.MockTransport`. `marimo` and + `nbformat` are in the dev group only, so the helpers' tests drive the real `mo.md`, `mo.Html` + and nbformat schema; the package never imports either at module load. The checks CI runs (`.github/workflows/ci.yml`): diff --git a/README.md b/README.md index 34c35d3..9375eac 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,81 @@ pins a CLI that accepts it, `verify` drops a bundle's top-level `trustRegistry` sees the bundle, and changes nothing else. A test pins that the CLI receives the same document minus that one key. +## Notebook helpers + +Five helpers arrange and render around the CLI. None of them computes one of the format's hashes, +reads the seed, or verifies anything itself. + +```python +import io +import pandas as pd +import typedstandards as ts + +# Before signing: pin each input, and add the reader's badge and the comparison cell. +content, entry = ts.pin("https://data.example.org/resource/abcd-1234.csv", licence="CC-BY-4.0") +frame = pd.read_csv(io.BytesIO(content)) + +ts.badge_cell( + "https://records.example.org/bundles/analysis.bundle.json", + capture_method="script-run", + notebook="analysis.ipynb", +) +ts.comparison_cell( + "analysis.ipynb", + {"rows": 1204, "mean_fare": 13.75}, + recompute="recompute_key_metrics()", + captured_at="2026-10-03T12:00:00Z", +) + +# Sign (record is an envelope input like the one under Use), then serve and show. +signed = ts.sign({**record, "queries": [entry]}, output_file="analysis.ipynb") +bundle = ts.view(signed, visibility="public", title="Example analysis") +ts.sidecar(bundle, "analysis.ipynb") # writes analysis.ipynb.record.yaml +ts.show(bundle) # in Jupyter; ts.show(bundle, marimo=True) in Marimo +``` + +- **`pin(url, *, licence=None, dataset_id=None, portal_metadata=None, save=None, ...)`** fetches + `url` once and returns `Pinned(content, entry)`: the response body, and a retrieval entry for + the record's `queries[]` with `url`, `sha256` (of the body), `bytes`, `httpStatus` and + `fetchedAt` (ISO 8601 UTC) under `arguments`. A URL shaped like an open-data portal resource + (`…/resource/[.ext]` or `…/api/views//rows.`, `` being `xxxx-xxxx`) gets a + second request to `/api/views/`, and the entry gains the portal's `rowsUpdatedAt` + (epoch seconds) and `datasetId`. A response that is not 2xx raises. `save=` writes the bytes + to that path and the entry to `.pin.json`. The SHA-256 is the one digest the package + computes: a signed assertion in `queries[]` that no check recomputes. +- **`badge_cell(bundle_url, *, capture_method, notebook=None, host=None, marimo=False)`** writes + the verifier badge, linked to `https://typedstandards.org/verify?url=` as `@typedstandards/host-core` writes it, above a two-row table (the host and + the capture method). With `notebook`, it is inserted as the notebook's first cell (id + `typedstandards-badge` on nbformat 4.5). With `marimo=True`, it returns the source of a + `mo.md(...)` cell to paste into the app. The cell is written before signing and is part of the + signed bytes, so it names no hash and no time; a URL or value holding a 64-hex string, a date or + a time is refused. +- **`comparison_cell(notebook, values, *, recompute, captured_at)`** appends the comparison cell + of spec §8.7.4 as the last cell (id `typedstandards-comparison`): the values as Python literals, + `current = `, and a loop that prints each delta. Values are `None`, `bool`, `int`, + finite `float`, `str`, and lists and str-keyed dicts of those; anything else is refused. +- **`sidecar(view, artifact, *, directory=None)`** writes the commitment view as YAML (spec + §8.8.3) beside the artifact: every field `view` printed except the inline `package` and a + served bundle's `trustRegistry`, which are not §8.8.1 fields. The file is named + `.record.yaml`, extension kept (`analysis.ipynb.record.yaml`): the spec's + `` does not say whether the extension stays, and keeping it is the POSIX + basename and cannot collide when two artifacts share a stem (`analysis.ipynb` beside + `analysis.py`). +- **`show(record, result=None, *, role_path=("role",), marimo=False)`** renders a record (what + `view` or `sign` printed) with its `verify --json` result: the type, the role, the signer, + the hash, `createdAt`, the `vcsRef` (marked as asserted and not fetched), the status with its + reason or successor, one line per check, and a sentence saying that verification does not say + the analysis is correct. Without `result` it runs `verify`. In Jupyter it returns an object + with `_repr_html_`; with `marimo=True`, `mo.Html`. The role is a signed assertion the signer + made, read from the package's `extensions` at `role_path` (by default `extensions["role"]`, a + string or a list of strings); `show` labels it as the signer's and checks nothing about it. + +The notebook helpers edit the notebook as JSON, splicing the new cell into `cells` so every +other byte of the file stays as written. `httpx` is imported only inside `pin`, PyYAML only +inside `sidecar`, and `marimo` only inside a Marimo call; `IPython`, `marimo` and `nbformat` +are not dependencies. + ## Versions Each wrapper release pins one CLI version exactly. A CLI upgrade reaches users as a wrapper release diff --git a/pyproject.toml b/pyproject.toml index 705e9ee..5c1fb0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,11 @@ Homepage = "https://github.com/npstorey/typedstandards-python" Issues = "https://github.com/npstorey/typedstandards-python/issues" [dependency-groups] +# marimo and nbformat are development-only: the helpers' tests drive the real implementations +# (mo.md, mo.Html, nbformat's schema and writer). The package never imports either at module load. dev = [ + "marimo>=0.25.1", + "nbformat>=5.11.1", "pytest>=8.4", "ruff>=0.14", ] diff --git a/scripts/smoke_wheel.py b/scripts/smoke_wheel.py index 2c47989..9401003 100644 --- a/scripts/smoke_wheel.py +++ b/scripts/smoke_wheel.py @@ -5,8 +5,10 @@ TYPEDSTANDARDS_SIGNING_SEED_B64="$(openssl rand -base64 32)" /bin/python scripts/smoke_wheel.py -It checks the import, CLI_VERSION, the vendored CLI's --version, the vendored tree's licences, and -one sign-then-verify round trip (sign, view, verify) through the vendored CLI. It reads no seed. +It checks the import, CLI_VERSION, the vendored CLI's --version, the vendored tree's licences, +one sign-then-verify round trip (sign, view, verify) through the vendored CLI, and the five +helpers with the runtime dependencies the wheel declares (httpx for pin, PyYAML for sidecar), +offline: pin over httpx.MockTransport. It reads no seed. """ from __future__ import annotations @@ -14,6 +16,7 @@ import json import sys import sysconfig +import tempfile from pathlib import Path import typedstandards @@ -61,9 +64,46 @@ def main() -> int: print(f"signed {signed['envelopeHash']}; verify ok={result['ok']} status={result['lifecycle']['status']}") assert result["ok"] is True assert result["nodeId"] == signed["envelopeHash"] + helpers(record) print("smoke check passed") return 0 +def helpers(record: dict) -> None: + import httpx + + def portal(request: httpx.Request) -> httpx.Response: + if request.url.path == "/api/views/abcd-1234": + return httpx.Response(200, json={"rowsUpdatedAt": 1790991539}) + return httpx.Response(200, content=b"a,b\n1,2\n") + + content, entry = typedstandards.pin( + "https://data.example.org/resource/abcd-1234.csv", transport=httpx.MockTransport(portal) + ) + assert content == b"a,b\n1,2\n" and entry["arguments"]["rowsUpdatedAt"] == 1790991539, entry + + with tempfile.TemporaryDirectory() as tmp: + notebook = Path(tmp) / "analysis.ipynb" + cells = [{"cell_type": "code", "execution_count": None, "id": "a", "metadata": {}, "outputs": [], "source": []}] + document = {"cells": cells, "metadata": {}, "nbformat": 4, "nbformat_minor": 5} + notebook.write_text(json.dumps(document, indent=1) + "\n", encoding="utf-8") + typedstandards.badge_cell( + "https://records.example.org/bundles/analysis.bundle.json", capture_method="script-run", notebook=notebook + ) + typedstandards.comparison_cell( + notebook, {"rows": 2}, recompute="recompute()", captured_at="2026-10-03T00:00:00Z" + ) + inline = {k: v for k, v in record.items() if k != "output"} + signed = typedstandards.sign({**inline, "queries": [entry]}, output_file=notebook) + bundle = typedstandards.view(signed, visibility="public") + result = typedstandards.verify(bundle) + assert result["ok"] is True + yaml_path = typedstandards.sidecar(bundle, notebook) + assert yaml_path.name == "analysis.ipynb.record.yaml", yaml_path + html = typedstandards.show(bundle, result)._repr_html_() + assert "Signed with a self-certifying key" in html + print("helpers: pin, badge_cell, comparison_cell, sidecar and show ran from the installed wheel") + + if __name__ == "__main__": sys.exit(main()) diff --git a/src/typedstandards/__init__.py b/src/typedstandards/__init__.py index 776a9eb..5440011 100644 --- a/src/typedstandards/__init__.py +++ b/src/typedstandards/__init__.py @@ -7,10 +7,14 @@ from __future__ import annotations +from ._badge import badge_cell from ._cli import cli_entry from ._cli import run as _run from ._commands import attest, sign, verify, view, withdraw +from ._comparison import comparison_cell from ._node import NODE_FLOOR, NODE_OVERRIDE, locate_node +from ._show import Shown, show +from ._sidecar import sidecar from .errors import ( CliError, CliNotVendoredError, @@ -20,6 +24,7 @@ UsageError, VerificationError, ) +from .pin import Pinned, pin __version__ = "0.1.0.dev0" @@ -40,14 +45,21 @@ def cli_version() -> str: "CliNotVendoredError", "InternalError", "NodeLocatorError", + "Pinned", "SeedError", + "Shown", "UsageError", "VerificationError", "__version__", "attest", + "badge_cell", "cli_entry", "cli_version", + "comparison_cell", "locate_node", + "pin", + "show", + "sidecar", "sign", "verify", "view", diff --git a/src/typedstandards/_badge.py b/src/typedstandards/_badge.py new file mode 100644 index 0000000..0c03802 --- /dev/null +++ b/src/typedstandards/_badge.py @@ -0,0 +1,155 @@ +"""``badge_cell``: the verifier badge as a notebook's first cell (Jupyter) or a ``mo.md`` cell (Marimo). + +The link and the badge follow ``@typedstandards/host-core``'s ``links.ts`` (typedstandards +``116882a``, ``packages/host-core/src/links.ts``): ``buildVerifyHref`` (:42-48) puts a hosted +bundle URL through ``?url=`` percent-encoded with ``encodeURIComponent``, and +``buildEmbedMarkdown`` (:70-79) writes a linked image whose destination is in angle brackets. +The constants are copied from :15-28. ``tests/test_badge.py`` compares this module's output with +strings captured from host-core's own functions. + +The cell is written before the notebook is signed, and under ``raw-bytes/v1`` the notebook's +bytes are the record's output, so the cell can carry only facts known by name before signing: +the badge linked to the bundle URL, the host and the capture method. It never carries a hash or +a time; the verifier shows those from the signed record. Spec §8.8.4 calls such a cell "purely a +reader affordance". +""" + +from __future__ import annotations + +import os +import re +from urllib.parse import quote, urlsplit + +from ._notebook import insert_cell, source_lines + +#: host-core links.ts:16, the verifier's canonical origin. +CANONICAL_ORIGIN = "https://typedstandards.org" +#: links.ts:19. +BADGE_ASSET_PATH = "/badge/typed-standards-verify.svg" +#: links.ts:22-23. +BADGE_WIDTH = 248 +BADGE_HEIGHT = 30 +#: links.ts:28: it describes the action, not a verdict. +BADGE_ALT = "Verify this record with Typed Standards" + +#: The characters ``encodeURIComponent`` leaves unescaped besides ASCII letters and digits. +_URI_COMPONENT_SAFE = "-_.!~*'()" + +#: The badge cell's id (nbformat 4.5). +BADGE_CELL_ID = "typedstandards-badge" + +_HEX64 = re.compile(r"[0-9a-fA-F]{64}") +_DATE_OR_TIME = re.compile(r"\d{4}-\d{2}-\d{2}|\d{2}:\d{2}(?::\d{2})?") +_UNSAFE_IN_TABLE = re.compile(r"[|`\\\"\x00-\x1f\x7f]") + + +def encode_uri_component(value: str) -> str: + """JavaScript's ``encodeURIComponent``: UTF-8, every byte escaped but ``A-Z a-z 0-9 - _ . ! ~ * ' ( )``.""" + return quote(value, safe=_URI_COMPONENT_SAFE) + + +def verify_href(bundle_url: str) -> str: + """The verifier deep link for a hosted bundle: ``https://typedstandards.org/verify?url=``.""" + url = bundle_url.strip() + if not re.match(r"https?://", url, re.IGNORECASE): + raise ValueError(f"bundle_url must be an http(s) URL of a served bundle, not {bundle_url!r}") + return f"{CANONICAL_ORIGIN}/verify?url={encode_uri_component(url)}" + + +def badge_markdown(bundle_url: str) -> str: + """host-core's ``buildEmbedMarkdown`` for a hosted bundle (light theme).""" + return f"[![{BADGE_ALT}]({CANONICAL_ORIGIN}{BADGE_ASSET_PATH})](<{verify_href(bundle_url)}>)" + + +def _check_fact(name: str, value: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} must be a non-empty string") + if _UNSAFE_IN_TABLE.search(value): + raise ValueError( + f"{name} {value!r} holds a character the cell's table cannot carry (|, `, \\, \" or a control)" + ) + return value.strip() + + +def refuse_hash_or_time(text: str) -> str: + """Raise ``ValueError`` when ``text`` holds a 64-hex string, a date or a time of day. + + The badge cell is part of the signed bytes, written before signing: a hash in it cannot be + the record's own, and a time in it cannot be the signing time, so either would mislead. + """ + if _HEX64.search(text): + raise ValueError( + "the badge cell would carry a 64-hex string; it is written before signing, so it names no hash" + ) + if _DATE_OR_TIME.search(text): + raise ValueError("the badge cell would carry a date or time; it is written before signing, so it names no time") + return text + + +def badge_text(bundle_url: str, *, capture_method: str, host: str | None = None) -> str: + """The badge cell's Markdown: the badge, a two-row table (host, capture method), one sentence.""" + if host is None: + host = urlsplit(bundle_url.strip()).netloc + host = _check_fact("host", host) + capture_method = _check_fact("capture_method", capture_method) + text = ( + f"{badge_markdown(bundle_url)}\n" + "\n" + "| Typed Standards record | |\n" + "|---|---|\n" + f"| Host | `{host}` |\n" + f"| Capture method | `{capture_method}` |\n" + "\n" + "This cell is a reader affordance and is not authoritative: verification reads the signed record, " + "not this cell, and the verifier shows the record's signer, hash and time.\n" + ) + return refuse_hash_or_time(text) + + +def _marimo_source(markdown: str) -> str: + """A Marimo cell's source that renders ``markdown`` with ``mo.md``.""" + if '"""' not in markdown and "\\" not in markdown and not markdown.endswith('"'): + return f'mo.md(\n """{markdown}"""\n)\n' + return f"mo.md({markdown!r})\n" + + +def badge_cell( + bundle_url: str, + *, + capture_method: str, + notebook: str | os.PathLike[str] | None = None, + host: str | None = None, + marimo: bool = False, + cell_id: str = BADGE_CELL_ID, +) -> str: + """The verifier badge as a notebook cell, written before signing. + + ``bundle_url`` is where the record's bundle will be served (for example + ``https:///bundles/.bundle.json``). ``capture_method`` is the record's + ``captureMethod`` (for example ``script-run``). ``host`` defaults to the bundle URL's host. + + Jupyter (the default): with ``notebook``, the cell is inserted as the notebook's first cell, + a markdown cell with id ``cell_id`` on nbformat 4.5 or later, and every other byte of the + file is kept. Returns the cell's Markdown. An existing cell with the same id is refused, so + a second call does not add a second badge. + + Marimo (``marimo=True``): returns the source of a cell that renders the same Markdown with + ``mo.md``, to paste into the app. ``notebook`` is refused, since a Marimo app is Python + source, not a notebook file. + + The cell names no hash and no time: a URL or fact holding a 64-hex string, a date or a time + is refused with ``ValueError``. + """ + text = badge_text(bundle_url, capture_method=capture_method, host=host) + if marimo: + if notebook is not None: + raise ValueError("marimo=True returns a cell's source; it does not edit a notebook file") + return _marimo_source(text) + if notebook is not None: + insert_cell( + notebook, + {"cell_type": "markdown", "metadata": {}, "source": source_lines(text)}, + where="first", + cell_id=cell_id, + ) + return text diff --git a/src/typedstandards/_comparison.py b/src/typedstandards/_comparison.py new file mode 100644 index 0000000..fee6b94 --- /dev/null +++ b/src/typedstandards/_comparison.py @@ -0,0 +1,164 @@ +"""``comparison_cell``: the spec §8.7.4 comparison cell, appended as a notebook's last cell. + +The cell embeds the values an execution produced as Python literals, recomputes them with the +caller's expression, and prints each delta, in the canonical shape of spec §8.7.4 (the hub's +``docs/architecture/typed-standards-specification.md``, lines 860-874 at ``6957a3a``):: + + # ORIGINAL VALUES (captured at executedAt = ) + original = { + "": , + } + + # CURRENT VALUES (re-computed against live data using the same helpers + queries above) + current = + + # DELTAS + for k in original: + delta = ... + print(...) + +The spec's block indents by one space; the cell indents by four, as Python code usually is. The +cell is appended before signing, so it is inside the signed bytes. +""" + +from __future__ import annotations + +import ast +import json +import math +import os +from collections.abc import Mapping +from datetime import UTC, datetime +from typing import Any + +from ._notebook import insert_cell, source_lines + +#: The comparison cell's id (nbformat 4.5). +COMPARISON_CELL_ID = "typedstandards-comparison" + +_CURRENT_COMMENT = "# CURRENT VALUES (re-computed against live data using the same helpers + queries above)" +_DELTAS = ( + "# DELTAS\n" + "for k in original:\n" + " delta = (current[k] - original[k]) if isinstance(original[k], (int, float)) else (original[k], current[k])\n" + ' print(f"{k}: original={original[k]}, current={current[k]}, delta={delta}")\n' +) + + +def _string(value: str, where: str) -> str: + try: + value.encode("utf-8") + except UnicodeEncodeError as err: + raise ValueError(f"{where} is a string that is not valid UTF-8 text") from err + return json.dumps(value, ensure_ascii=False) # a JSON string is also a Python string literal + + +def literal(value: Any, where: str = "value") -> str: + """``value`` as Python literal source. Admits ``None``, ``bool``, ``int``, finite ``float``, + ``str``, and lists and str-keyed dicts of those; a subclass of ``int``, ``float`` or ``str`` + (a NumPy float, an ``IntEnum``) is written as its base value. Anything else raises + ``TypeError``, and a non-finite float ``ValueError``.""" + if value is None: + return "None" + if isinstance(value, bool): + return "True" if value else "False" + if isinstance(value, int): + return repr(int(value)) + if isinstance(value, float): + number = float(value) + if not math.isfinite(number): + raise ValueError(f"{where} is {number!r}, which has no Python literal") + return repr(number) + if isinstance(value, str): + return _string(str(value), where) + if type(value) is list: + return "[" + ", ".join(literal(item, f"{where}[{i}]") for i, item in enumerate(value)) + "]" + if type(value) is dict: + parts = [] + for key, item in value.items(): + if not isinstance(key, str): + raise TypeError(f"{where} has a key {key!r} that is not a string") + parts.append(f"{_string(str(key), where)}: {literal(item, f'{where}[{key!r}]')}") + return "{" + ", ".join(parts) + "}" + raise TypeError( + f"{where} is a {type(value).__name__}, not a literal: comparison values are None, bool, int, float, " + "str, or lists and str-keyed dicts of those" + ) + + +def _captured_at(value: str | datetime) -> str: + if isinstance(value, datetime): + if value.tzinfo is None: + raise ValueError("captured_at must be timezone-aware") + return value.astimezone(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") + if not isinstance(value, str) or "\n" in value or "\r" in value: + raise ValueError("captured_at must be an ISO 8601 timestamp on one line") + try: + datetime.fromisoformat(value) + except ValueError as err: + raise ValueError(f"captured_at {value!r} is not an ISO 8601 timestamp") from err + return value + + +def _recompute(expression: str) -> str: + if not isinstance(expression, str) or not expression.strip() or "\n" in expression or "\r" in expression: + raise ValueError("recompute must be one line of Python: an expression that returns the current values") + try: + ast.parse(expression, mode="eval") + except SyntaxError as err: + raise ValueError(f"recompute {expression!r} is not a Python expression") from err + return expression.strip() + + +def comparison_source(values: Mapping[str, Any], *, recompute: str, captured_at: str | datetime) -> str: + """The comparison cell's source, without writing it anywhere.""" + if not isinstance(values, Mapping) or not values: + raise ValueError("values must be a non-empty mapping of names to literal values") + items = [] + for name, value in values.items(): + if not isinstance(name, str) or not name: + raise TypeError(f"value names must be non-empty strings, not {name!r}") + items.append(f" {_string(name, 'a value name')}: {literal(value, repr(name))},\n") + original = "original = {\n" + "".join(items) + "}\n" + if ast.literal_eval(original.split("=", 1)[1].strip()) != dict(values): # pragma: no cover - a defect here + raise AssertionError("the literal does not read back as the values") + return ( + f"# ORIGINAL VALUES (captured at executedAt = {_captured_at(captured_at)})\n" + f"{original}" + "\n" + f"{_CURRENT_COMMENT}\n" + f"current = {_recompute(recompute)}\n" + "\n" + f"{_DELTAS}" + ) + + +def comparison_cell( + notebook: str | os.PathLike[str], + values: Mapping[str, Any], + *, + recompute: str, + captured_at: str | datetime, + cell_id: str = COMPARISON_CELL_ID, +) -> str: + """Append the spec §8.7.4 comparison cell to ``notebook`` as its last cell; return its source. + + ``values`` names the values the execution produced, each a literal (``None``, ``bool``, + ``int``, finite ``float``, ``str``, or lists and str-keyed dicts of those); anything else is + refused. ``recompute`` is one line of Python, an expression that returns a mapping with the + same names, run against live data when the notebook is re-executed (for example + ``recompute_key_metrics()``). ``captured_at`` is the execution's time, an ISO 8601 string or + a timezone-aware ``datetime``. + + The cell is a code cell with id ``cell_id`` (nbformat 4.5 or later), no outputs and no + execution count, and every other byte of the notebook is kept. Append it after executing the + notebook and before signing it: the cell is part of the signed bytes. + """ + source = comparison_source(values, recompute=recompute, captured_at=captured_at) + insert_cell( + notebook, + {"cell_type": "code", "execution_count": None, "metadata": {}, "outputs": [], "source": source_lines(source)}, + where="last", + cell_id=cell_id, + ) + return source diff --git a/src/typedstandards/_notebook.py b/src/typedstandards/_notebook.py new file mode 100644 index 0000000..6da1029 --- /dev/null +++ b/src/typedstandards/_notebook.py @@ -0,0 +1,200 @@ +"""Insert one cell into a Jupyter notebook's ``cells``, keeping every other byte as written. + +The notebook is read as JSON text, and the new cell is spliced into the ``cells`` array at its +start or its end. Nothing else is re-serialized, so key order, indentation, line endings, number +spelling and ``\\u`` escapes elsewhere in the file stay exactly as the author's tool wrote them. +The new cell itself is written in the file's own style: its indentation unit (nbformat writes 1), +its line ending, and ``ensure_ascii`` as found. Its keys are sorted, as nbformat writes them. + +nbformat 4.5 (``nbformat_minor`` 5 or later) requires each cell to carry an ``id`` of 1 to 64 +characters from ``[a-zA-Z0-9-_]``, unique in the notebook. The helpers give their cells fixed, +readable ids, so the same call on the same notebook writes the same bytes; an id already in the +notebook is refused, never replaced or suffixed. A notebook below 4.5 gets no ``id``, which its +schema does not allow. +""" + +from __future__ import annotations + +import json +import os +import re +from pathlib import Path +from typing import Any, Literal + +CELL_ID = re.compile(r"[a-zA-Z0-9_-]{1,64}") +_WS = re.compile(r"[ \t\r\n]*") +_NON_ASCII_ESCAPE = re.compile(r"\\u(?:00[89a-fA-F][0-9a-fA-F]|0[1-9a-fA-F][0-9a-fA-F]{2}|[1-9a-fA-F][0-9a-fA-F]{3})") +_DECODER = json.JSONDecoder() + + +class _Layout: + """Where ``cells`` and each of its elements sit in the notebook's text.""" + + def __init__(self, text: str) -> None: + self.text = text + self.top_ws = "" # whitespace between "{" and the first key + self.first_value_ws = "" # whitespace between the first key's ":" and its value + self.first_item_ws: str | None = None # whitespace after the "," that ends the first member + self.cells_indent = "" # the indentation of the line holding the "cells" key + self.cells_open = -1 # index just after "[" + self.cells_close = -1 # index of "]" + self.spans: list[tuple[int, int]] = [] + self._scan() + + def _ws(self, i: int) -> int: + return _WS.match(self.text, i).end() # type: ignore[union-attr] + + def _scan(self) -> None: + text = self.text + i = self._ws(0) + if text[i : i + 1] != "{": + raise ValueError("the notebook is not a JSON object") + start = i + 1 + i = self._ws(start) + self.top_ws = text[start:i] + first = True + while text[i : i + 1] != "}": + key_start = i + if text[i : i + 1] != '"': + raise ValueError("the notebook's JSON is malformed") + key, i = json.decoder.scanstring(text, i + 1) + i = self._ws(i) + if text[i : i + 1] != ":": + raise ValueError("the notebook's JSON is malformed") + colon = i + 1 + i = self._ws(colon) + if first: + self.first_value_ws = text[colon:i] + if key == "cells": + line_start = text.rfind("\n", 0, key_start) + 1 + self.cells_indent = text[line_start:key_start] if line_start else "" + i = self._scan_cells(i) + else: + _, i = _DECODER.raw_decode(text, i) + i = self._ws(i) + if text[i : i + 1] == ",": + after = self._ws(i + 1) + if first: + self.first_item_ws = text[i + 1 : after] + i = after + first = False + if self.cells_open < 0: + raise ValueError("the notebook has no cells array") + + def _scan_cells(self, i: int) -> int: + text = self.text + if text[i : i + 1] != "[": + raise ValueError("the notebook's cells is not an array") + self.cells_open = i + 1 + i = self._ws(i + 1) + while text[i : i + 1] != "]": + start = i + _, i = _DECODER.raw_decode(text, i) + self.spans.append((start, i)) + i = self._ws(i) + if text[i : i + 1] == ",": + i = self._ws(i + 1) + self.cells_close = i + return i + 1 + + +def _newline(layout: _Layout) -> str: + return "\r\n" if "\r\n" in layout.text else "\n" + + +def _ensure_ascii(text: str) -> bool: + """The file's ``ensure_ascii``: False when it holds a raw non-ASCII character, True when it + escapes one as ``\\u....`` and holds none raw, and False (nbformat's choice) otherwise.""" + if any(ord(ch) > 0x7F for ch in text): + return False + return _NON_ASCII_ESCAPE.search(text) is not None + + +def _serialize(cell: dict[str, Any], layout: _Layout, base_indent: str) -> str: + """The cell as JSON text in the file's style, its continuation lines indented by ``base_indent``.""" + ensure_ascii = _ensure_ascii(layout.text) + if "\n" in layout.top_ws: + unit = len(layout.top_ws.rsplit("\n", 1)[1]) + body = json.dumps(cell, indent=unit, ensure_ascii=ensure_ascii, sort_keys=True, separators=(",", ": ")) + return body.replace("\n", _newline(layout) + base_indent) + item_sep = "," + (layout.first_item_ws or "") + key_sep = ":" + layout.first_value_ws + return json.dumps(cell, ensure_ascii=ensure_ascii, sort_keys=True, separators=(item_sep, key_sep)) + + +def _indent_of(ws: str) -> str: + return ws.rsplit("\n", 1)[1] if "\n" in ws else "" + + +def read_notebook(path: str | os.PathLike[str]) -> tuple[str, dict[str, Any]]: + """The notebook's text and its parsed JSON; refuses anything but an nbformat 4 notebook.""" + raw = Path(path).read_bytes() + if raw.startswith(b"\xef\xbb\xbf"): + raise ValueError(f"{os.fspath(path)} starts with a UTF-8 byte-order mark, which nbformat does not write") + text = raw.decode("utf-8") + notebook = json.loads(text) + if not isinstance(notebook, dict) or notebook.get("nbformat") != 4 or not isinstance(notebook.get("cells"), list): + raise ValueError(f"{os.fspath(path)} is not an nbformat 4 notebook") + return text, notebook + + +def insert_cell( + path: str | os.PathLike[str], + cell: dict[str, Any], + *, + where: Literal["first", "last"], + cell_id: str, +) -> dict[str, Any]: + """Splice ``cell`` into the notebook at ``path`` as its first or last cell; return the cell + as written (with its ``id`` on nbformat 4.5 or later). + + Raises ``ValueError`` for a malformed ``cell_id``, or one another cell already has. + """ + text, notebook = read_notebook(path) + cell = dict(cell) + if notebook.get("nbformat_minor", 0) >= 5: + if not isinstance(cell_id, str) or CELL_ID.fullmatch(cell_id) is None: + raise ValueError(f"cell id {cell_id!r} is not 1 to 64 characters of [a-zA-Z0-9-_] (nbformat 4.5)") + taken = {c.get("id") for c in notebook["cells"] if isinstance(c, dict)} + if cell_id in taken: + raise ValueError( + f"the notebook already has a cell with id {cell_id!r}; pass another cell_id, or remove that cell first" + ) + cell["id"] = cell_id + + layout = _Layout(text) + nl = _newline(layout) + if layout.spans: + lead = text[layout.cells_open : layout.spans[0][0]] + base = _indent_of(lead) + if len(layout.spans) > 1: + sep = text[layout.spans[0][1] : layout.spans[1][0]] + else: + sep = "," + lead + body = _serialize(cell, layout, base) + if where == "first": + at = layout.spans[0][0] + new_text = text[:at] + body + sep + text[at:] + else: + at = layout.spans[-1][1] + new_text = text[:at] + sep + body + text[at:] + else: + if "\n" in layout.top_ws: + outer = layout.cells_indent if not layout.cells_indent.strip() else "" + base = outer + _indent_of(layout.top_ws) + body = nl + base + _serialize(cell, layout, base) + nl + outer + else: + body = _serialize(cell, layout, "") + new_text = text[: layout.cells_open] + body + text[layout.cells_close :] + + expected = dict(notebook) + expected["cells"] = [cell, *notebook["cells"]] if where == "first" else [*notebook["cells"], cell] + if json.loads(new_text) != expected: # pragma: no cover - a defect in the splice, never the input + raise AssertionError("the spliced notebook does not parse to the notebook plus the new cell") + Path(path).write_bytes(new_text.encode("utf-8")) + return cell + + +def source_lines(text: str) -> list[str]: + """``text`` as nbformat writes a cell's source: a list of lines, each keeping its newline.""" + return text.splitlines(keepends=True) diff --git a/src/typedstandards/_show.py b/src/typedstandards/_show.py new file mode 100644 index 0000000..2176d09 --- /dev/null +++ b/src/typedstandards/_show.py @@ -0,0 +1,381 @@ +"""``show``: render a record and what the CLI's ``verify --json`` reported for it, as HTML. + +``show`` renders and checks nothing itself. The record (a bundle ``view`` printed, or what +``sign`` printed) supplies the type, signer, hash, ``createdAt``, ``vcsRef``, ``summary`` and +``extensions``; the verify result (``{ok, nodeId, failures, checks, lifecycle}``) supplies every +check's reading and the lifecycle status. Without a result, ``show`` asks the CLI for one. + +The labels follow hub ADR-0030 §10 (``docs/adr/0030-self-certifying-signer-did-key.md`` at +``6957a3a``, lines 131-147): a key-derived signer reads "Signed with a self-certifying key", in +the normal tier and never beside a check-mark; ``displayName`` and ``bindingTier`` are the +signer's own description; check #14's ``key_derived_match`` reads "Signer identifier matches the +signing key"; continuity is "the same key". No line carries a check-mark. One sentence states, +as spec §9.3 does, that this does not say the analysis is correct. + +**The role** is a signed assertion the wrapper reads, not a check (Notebook Evidence D2). ``show`` +reads it from the package's ``extensions`` at ``role_path`` (default ``("role",)``, that is +``extensions["role"]``), a string or a list of strings, and labels it as asserted by the signer. + +Every string taken from the record or the result is HTML-escaped. The output depends only on +the inputs, so the same inputs give the same bytes. +""" + +from __future__ import annotations + +import json +import os +from collections.abc import Mapping, Sequence +from html import escape +from pathlib import Path +from typing import Any + +#: The one sentence spec §9.3 asks a verifier's display to carry, in this display's words. +NOT_CORRECTNESS = ( + "This shows that the record is intact and which key signed it; it does not say the analysis is correct." +) + +#: ADR-0030 §10's label and detail for a self-certifying key. +SELF_CERTIFIED_LABEL = "Signed with a self-certifying key" +SELF_CERTIFIED_DETAIL = ( + "The signer's identifier is derived from the signing key, so it proves that the same key signed " + "everything under this identifier, not who holds the key. No registry vouches for it, and the key " + "cannot be rotated or revoked." +) +#: ADR-0030 §10's label for check #14's key_derived_match. +KEY_DERIVED_MATCH_LABEL = "Signer identifier matches the signing key" + +_STRUCTURAL = frozenset( + { + "hashMatch", + "recomputedHash", + "nodeId", + "kid", + "hasSigning", + "rekorVerified", + "rekorDetails", + "rekorInclusion", + "rfc3161", + "blobRefsVerified", + } +) + +_TABLE = "border-collapse: collapse; margin: 0.25em 0;" +_TH = "text-align: left; padding: 0.15em 1em 0.15em 0; vertical-align: top; font-weight: 600;" +_TD = "text-align: left; padding: 0.15em 0; vertical-align: top;" +_NOTE = "color: #555;" + + +class Shown: + """What :func:`show` returns for Jupyter: an object whose ``_repr_html_`` is the HTML.""" + + __slots__ = ("html",) + + def __init__(self, html: str) -> None: + self.html = html + + def _repr_html_(self) -> str: + return self.html + + def __repr__(self) -> str: + return f"" + + +def _e(value: Any) -> str: + """HTML-escape any value taken from the record or the result.""" + if isinstance(value, str): + return escape(value, quote=True) + return escape(json.dumps(value, ensure_ascii=False, sort_keys=True), quote=True) + + +def _code(value: Any, title: Any = None) -> str: + attr = f' title="{_e(title)}"' if title is not None else "" + return f"{_e(value)}" + + +def _note(text: str) -> str: + return f'{text}' + + +def _abbreviate(identifier: str) -> str: + if identifier.startswith("did:key:") and len(identifier) > 24: + key = identifier[len("did:key:") :] + return f"did:key:{key[:8]}…{key[-6:]}" + if len(identifier) > 40: + return f"{identifier[:28]}…{identifier[-8:]}" + return identifier + + +def _get(mapping: Any, *path: str) -> Any: + for key in path: + if not isinstance(mapping, Mapping): + return None + mapping = mapping.get(key) + return mapping + + +def _load(value: Mapping[str, Any] | str | os.PathLike[str]) -> Mapping[str, Any]: + if isinstance(value, Mapping): + return value + loaded = json.loads(Path(value).read_bytes()) + if not isinstance(loaded, Mapping): + raise ValueError(f"{os.fspath(value)} is not a JSON object") + return loaded + + +def _role(package: Mapping[str, Any], role_path: Sequence[str]) -> str | None: + value = _get(package.get("extensions"), *role_path) + if isinstance(value, str) and value: + return value + if isinstance(value, list) and value and all(isinstance(v, str) for v in value): + return ", ".join(value) + return None + + +def _status(lifecycle: Mapping[str, Any]) -> str: + status = lifecycle.get("status") + if status == "withdrawn": + text = f"{_code('withdrawn')}, reason: {_e(lifecycle.get('withdrawnReason') or '(none given)')}" + if lifecycle.get("withdrawnAt"): + text += f" {_note('(at ' + _e(lifecycle['withdrawnAt']) + ')')}" + return text + if status == "superseded": + successor = lifecycle.get("successorNodeId") + text = f"{_code('superseded')}, successor: " + text += _code(str(successor)[:12] + "…", successor) if successor else "(not named)" + if lifecycle.get("supersededAt"): + text += f" {_note('(at ' + _e(lifecycle['supersededAt']) + ')')}" + return text + if status is None: + return _note("not reported") + return _code(status) + + +def _check_rows(checks: Mapping[str, Any], package: Mapping[str, Any]) -> list[str]: + rows: list[tuple[str, str]] = [] + + def add(name: str, reading: str) -> None: + rows.append((name, reading)) + + def status_of(key: str) -> Any: + return _get(checks, key, "status") + + if "envelopeIntegrity" in checks: + add("#1 Envelope integrity", _code(status_of("envelopeIntegrity"))) + if "signatureValid" in checks: + valid = checks["signatureValid"] + add("#2 Signature", _code({True: "valid", False: "invalid"}.get(valid, "no signature"))) + if "contentCanonicalization" in checks: + rule = _get(checks, "contentCanonicalization", "rule") + add( + "#3 Content canonicalization", + _code(status_of("contentCanonicalization")) + (f", {_code(rule)}" if rule else ""), + ) + if "contentHash" in checks: + matched = _get(checks, "contentHash", "matched") + add("#4 Content hash", _code(status_of("contentHash")) + (f", {_code(matched)}" if matched else "")) + if "keyTrust" in checks: + trust = status_of("keyTrust") + if trust == "self_certified": + add("#5 Key trust", f"{SELF_CERTIFIED_LABEL} {_note('(' + _code(trust) + '; no registry vouches for it)')}") + else: + verified = _get(checks, "keyTrust", "verified") + add( + "#5 Key trust", + _code(trust) + (f" {_note('(verified: ' + _e(verified) + ')')}" if verified is not None else ""), + ) + if "signingKeyIdConsistency" in checks: + add("#6 Signing key id", _code(status_of("signingKeyIdConsistency"))) + if "hasTimestamp" in checks: + rfc3161 = checks.get("rfc3161") + if not checks["hasTimestamp"]: + add("#7 Timestamp", _note("no RFC 3161 token carried")) + else: + add("#7 Timestamp", _code(_get(rfc3161, "status") or "token carried")) + if "hasRekor" in checks: + if not checks["hasRekor"]: + add("#8 Transparency log", _note("no log entry carried")) + else: + add( + "#8 Transparency log", + _code({True: "verified", False: "not verified"}.get(checks.get("rekorVerified"), "entry carried")), + ) + if "blobRefs" in checks: + refs = checks["blobRefs"] if isinstance(checks["blobRefs"], list) else [] + if not refs: + add("#9 Referenced files", _note("none")) + else: + readings = ", ".join(_code(_get(r, "status") or _get(r, "reason") or r) for r in refs) + add("#9 Referenced files", f"{len(refs)}: {readings}") + if "lifecycle" in checks: + source = _get(checks, "lifecycle", "source") + add( + "#10 Lifecycle", + _code(status_of("lifecycle")) + (f" {_note('(source: ' + _e(source) + ')')}" if source else ""), + ) + capture = _get(checks, "captureMethodVocab", "captureMethod") or _get(package, "metadata", "captureMethod") + if capture: + add("#11 Capture method", _code(capture)) + if "typeResolution" in checks: + add("#12 Type", _code(status_of("typeResolution")) + f", {_code(_get(checks, 'typeResolution', 'type'))}") + if "signerIdentity" in checks: + identity = status_of("signerIdentity") + if identity == "key_derived_match": + add("#14 Signer identity", f"{KEY_DERIVED_MATCH_LABEL} {_note('(' + _code(identity) + ')')}") + else: + add("#14 Signer identity", _code(identity)) + if "captureMethodVocab" in checks: + profile = _get(checks, "captureMethodVocab", "profileType") + add( + "#15 Capture method vocabulary", + _code(status_of("captureMethodVocab")) + (f", {_code(profile)}" if profile else ""), + ) + if "contentProfile" in checks: + add("#16 Content profile", _code(status_of("contentProfile"))) + + known = { + "envelopeIntegrity", "signatureValid", "contentCanonicalization", "contentHash", "keyTrust", + "signingKeyIdConsistency", "hasTimestamp", "hasRekor", "blobRefs", "lifecycle", "typeResolution", + "signerIdentity", "captureMethodVocab", "contentProfile", + } # fmt: skip + for key in sorted(k for k in checks if k not in known and k not in _STRUCTURAL): + value = checks[key] + add(_e(key), _code(_get(value, "status") if isinstance(value, Mapping) and "status" in value else value)) + return [f"
  • {name}: {reading}
  • " for name, reading in rows] + + +def render( + record: Mapping[str, Any], + result: Mapping[str, Any], + *, + role_path: Sequence[str] = ("role",), +) -> str: + """The HTML for ``record`` and its verify ``result``.""" + package = record.get("package") + if not isinstance(package, Mapping): + raise ValueError("show takes what view or sign printed: a record with its package inline") + checks = result.get("checks") if isinstance(result.get("checks"), Mapping) else {} + signer = package.get("signer") if isinstance(package.get("signer"), Mapping) else {} + identifier = signer.get("identifier") + key_status = _get(checks, "keyTrust", "status") + record_hash = record.get("packageHash") or record.get("envelopeHash") + node_id = result.get("nodeId") or record_hash + + rows: list[tuple[str, str]] = [] + record_type = package.get("type") or _get(checks, "typeResolution", "type") + rows.append(("Type", _code(record_type) if record_type else _note("none named; read as content/analysis/v1"))) + role = _role(package, role_path) + where = "extensions" + "".join(f"[{json.dumps(k)}]" for k in role_path) + rows.append( + ( + "Role", + f"{_e(role)} {_note('(asserted by the signer in ' + _e(where) + '; not checked)')}" + if role + else _note(f"none asserted in {_e(where)}"), + ) + ) + rows.append(("Status", _status(result.get("lifecycle") or {}))) + + if isinstance(identifier, str): + signer_cell = _code(_abbreviate(identifier), identifier) + if key_status == "self_certified": + signer_cell += f" · {SELF_CERTIFIED_LABEL}
    {_note(SELF_CERTIFIED_DETAIL)}" + elif key_status: + signer_cell += f" · key status {_code(key_status)}" + else: + signer_cell = _note("no signer identifier in the record") + rows.append(("Signer", signer_cell)) + if signer.get("displayName"): + rows.append( + ( + "Name", + f"calls itself “{_e(signer['displayName'])}” " + _note("(the signer’s own description; not verified)"), + ) + ) + if signer.get("bindingTier"): + rows.append( + ( + "Binding tier", + f"describes itself as {_code(signer['bindingTier'])} " + + _note("(the signer’s own description; no check establishes it)"), + ) + ) + if node_id: + rows.append(("Envelope hash", _code(str(node_id)[:12] + "…", node_id))) + if record_hash and node_id != record_hash: + rows.append(("Record names", _code(str(record_hash)[:12] + "…", record_hash))) + created = _get(package, "metadata", "createdAt") + if created: + rows.append(("Created", _code(created) + f" {_note('(the signer’s clock)')}")) + vcs = package.get("vcsRef") + if isinstance(vcs, Mapping): + parts = [_code(vcs.get("repoUrl"))] + if vcs.get("commitSha"): + parts.append(f"commit {_code(vcs['commitSha'])}") + if vcs.get("path"): + parts.append(f"path {_code(vcs['path'])}") + if vcs.get("ref"): + parts.append(f"ref {_code(vcs['ref'])}") + rows.append(("Source revision", ", ".join(parts) + f" {_note('(asserted; not fetched)')}")) + summary = package.get("summary") + if isinstance(summary, str) and summary: + rows.append(("Summary", f"{_e(summary)} {_note('(the signer’s)')}")) + + if result.get("ok") is True: + verdict = f"{_code('verify')}: ok, no check contradicts the record" + else: + failures = result.get("failures") or [] + listed = ", ".join(_code(f) for f in failures) if failures else _note("none listed") + verdict = f"{_code('verify')}: failed, {listed}" + + title = record.get("subjectTitle") + heading = "Typed Standards record" + (f": {_e(title)}" if isinstance(title, str) and title else "") + table = "".join(f'{name}{cell}' for name, cell in rows) + checks_html = "".join(_check_rows(checks, package)) + return ( + '
    ' + f'

    {heading}

    ' + f'{table}
    ' + f'

    {verdict}

    ' + f'
      {checks_html}
    ' + f'

    {NOT_CORRECTNESS}

    ' + "
    " + ) + + +def show( + record: Mapping[str, Any] | str | os.PathLike[str], + result: Mapping[str, Any] | str | os.PathLike[str] | None = None, + *, + role_path: Sequence[str] = ("role",), + marimo: bool = False, +) -> Any: + """Render ``record`` with what ``verify --json`` reported for it. + + ``record`` is what :func:`~typedstandards.view` printed (a bundle) or what + :func:`~typedstandards.sign` printed, as a mapping or the path of its JSON. ``result`` is a + verify result for it (``typedstandards.verify(record)``, or a saved one); without it, ``show`` + runs :func:`~typedstandards.verify`, which needs Node, and renders a failed verdict too. + + Jupyter (the default) returns a :class:`Shown`, whose ``_repr_html_`` is the HTML. Marimo + (``marimo=True``) returns ``mo.Html`` of the same HTML; ``marimo`` is imported only then. + ``role_path`` names where the role sits under the package's ``extensions``. + """ + loaded = _load(record) + if result is None: + from ._commands import verify + from .errors import VerificationError + + try: + verdict: Mapping[str, Any] = verify(dict(loaded)) + except VerificationError as err: + if not isinstance(err.document, Mapping): + raise + verdict = err.document + else: + verdict = _load(result) + html = render(loaded, verdict, role_path=role_path) + if marimo: + import marimo as mo + + return mo.Html(html) + return Shown(html) diff --git a/src/typedstandards/_sidecar.py b/src/typedstandards/_sidecar.py new file mode 100644 index 0000000..7ab1d90 --- /dev/null +++ b/src/typedstandards/_sidecar.py @@ -0,0 +1,102 @@ +"""``sidecar``: the commitment view as a sibling YAML file (spec §8.8.3). + +Spec §8.8.3 names the file ``.record.yaml`` and fills it with the §8.8.1 +field set at the top level. ``view`` prints the §8.8.1 view plus the inline ``package``, and a +bundle served by ``@typedstandards/host-core`` also carries ``trustRegistry``. Neither of those +two is a §8.8.1 field (§8.8.1 calls them the two fields the self-contained serialization +inlines), so the sidecar leaves both out and carries every other field ``view`` printed, in the +order it printed them. + +**The basename.** The spec does not say whether ```` keeps the artifact's +extension. The sidecar keeps it: ``analysis.ipynb`` gets ``analysis.ipynb.record.yaml``. That is +the POSIX ``basename`` of the artifact's path; it cannot collide when two artifacts share a stem +(a notebook ``analysis.ipynb`` beside a dashboard source ``analysis.py``); and the artifact's +name is the sidecar's name minus ``.record.yaml``. +""" + +from __future__ import annotations + +import json +import os +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +#: Spec §8.8.1's fields, in its table's order (``evidenceProtocolVersion`` is the dual-era key). +COMMITMENT_VIEW_FIELDS = ( + "protocolVersion", + "evidenceProtocolVersion", + "packageHash", + "packageUrl", + "visibility", + "captureMethod", + "contentProfile", + "producerProfile", + "type", + "signer", + "contentHash", + "contentCanonicalization", + "signature", + "signerIdentity", + "rfc3161Timestamp", + "rekorEntryId", + "rekorInclusionProof", + "rekorEntryBody", + "lifecycle", + "lifecycleAttestations", + "attestations", + "trustRegistryUrl", + "trustRegistryUrlLegacy", + "subjectTitle", + "subjectSummary", +) + +#: What ``view`` and a served bundle inline beyond §8.8.1; the sidecar leaves these out. +INLINED = ("package", "trustRegistry") + +SUFFIX = ".record.yaml" + + +def sidecar_name(artifact: str | os.PathLike[str]) -> str: + """``.record.yaml``: ``analysis.ipynb`` gives ``analysis.ipynb.record.yaml``.""" + name = Path(artifact).name + if not name: + raise ValueError(f"{os.fspath(artifact)!r} names no file") + return name + SUFFIX + + +def commitment_fields(view: Mapping[str, Any]) -> dict[str, Any]: + """``view`` without ``package`` and ``trustRegistry``, in the order ``view`` printed it.""" + if not isinstance(view, Mapping) or "packageHash" not in view: + raise ValueError("sidecar takes what view printed (a commitment view, with packageHash)") + return {key: value for key, value in view.items() if key not in INLINED} + + +def sidecar( + view: Mapping[str, Any] | str | os.PathLike[str], + artifact: str | os.PathLike[str], + *, + directory: str | os.PathLike[str] | None = None, +) -> Path: + """Write ``.record.yaml`` from what :func:`~typedstandards.view` printed. + + ``view`` is the view (or a bundle a host serves), as a mapping or the path of its JSON. + ``artifact`` is the signed file; only its name is used, and the sidecar is written beside it + unless ``directory`` names another place. Returns the sidecar's path. + + The file holds every field of the view except ``package`` and ``trustRegistry``, in the + view's order, as YAML (``yaml.safe_dump``, Unicode kept). Every value loads back as the JSON + value it came from: a string such as ``"0.1.0"`` or a timestamp stays a string. + """ + import yaml + + if not isinstance(view, Mapping): + view = json.loads(Path(view).read_bytes()) + fields = commitment_fields(view) # type: ignore[arg-type] + text = yaml.safe_dump(fields, sort_keys=False, allow_unicode=True, default_flow_style=False, width=1 << 20) + if yaml.safe_load(text) != fields: # pragma: no cover - a defect in the dump, never the input + raise AssertionError("the YAML does not load back as the view's fields") + target = Path(directory) if directory is not None else Path(artifact).parent + path = target / sidecar_name(artifact) + path.write_text(text, encoding="utf-8") + return path diff --git a/src/typedstandards/pin.py b/src/typedstandards/pin.py new file mode 100644 index 0000000..749f8be --- /dev/null +++ b/src/typedstandards/pin.py @@ -0,0 +1,156 @@ +"""``pin``: fetch a URL once, and record what was fetched as a retrieval entry for ``queries[]``. + +This module is the one place the package computes a digest, and the guard tests allow it here +only. The SHA-256 is of the fetched body: a signed assertion in the record's ``queries[]`` that +no check recomputes (spec §8.7.5 item 7). It is none of the format's hashes: the content hash, +envelope hash and node id are the CLI's. + +The entry follows the core-satellite example's ``queries[]`` retrieval shape +(``package/build.mjs`` lines 280-285 at ``b40c30f``):: + + {"tool": ..., "operationType": "retrieve", + "arguments": {"url", "sha256", "bytes", "httpStatus", "fetchedAt", "licence"?, "rowsUpdatedAt"?}, + "datasetId"?: ...} + +An open-data portal resource is recognised by its URL's shape, not its host: +``…/resource/[.ext]`` or ``…/api/views//rows.``, with ```` matching +``[a-z0-9]{4}-[a-z0-9]{4}``. For one, ``pin`` also asks ``/api/views/`` for the +dataset's ``rowsUpdatedAt`` (epoch seconds, as the portal reports it) and records it with the +``datasetId``. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +from collections.abc import Callable +from datetime import UTC, datetime +from pathlib import Path +from typing import TYPE_CHECKING, Any, NamedTuple +from urllib.parse import urlsplit, urlunsplit + +if TYPE_CHECKING: # httpx is imported inside pin(), so importing the package does not load it + import httpx + +#: The ``tool`` an entry names by default. +PIN_TOOL = "typedstandards.pin" + +_PORTAL_PATH = re.compile( + r"(?:/resource/(?P[a-z0-9]{4}-[a-z0-9]{4})(?:\.[A-Za-z0-9]+)?|/api/views/(?P[a-z0-9]{4}-[a-z0-9]{4})/rows\.[A-Za-z0-9]+)$" +) + + +class Pinned(NamedTuple): + """What :func:`pin` returns: the fetched bytes, and the retrieval entry describing them.""" + + content: bytes + entry: dict[str, Any] + + +def portal_dataset_id(url: str) -> str | None: + """The dataset id when ``url`` has an open-data portal resource's shape, else ``None``.""" + match = _PORTAL_PATH.search(urlsplit(url).path) + return (match.group("a") or match.group("b")) if match else None + + +def _utc(moment: datetime) -> str: + if moment.tzinfo is None: + raise ValueError("the clock must return a timezone-aware datetime") + return moment.astimezone(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def _rows_updated_at(client: httpx.Client, url: str, dataset_id: str) -> Any: + parts = urlsplit(url) + meta_url = urlunsplit((parts.scheme, parts.netloc, f"/api/views/{dataset_id}", "", "")) + response = client.get(meta_url, headers={"Accept": "application/json"}) + response.raise_for_status() + try: + value = response.json()["rowsUpdatedAt"] + except (ValueError, KeyError, TypeError) as err: + raise ValueError( + f"{meta_url} did not report rowsUpdatedAt; pass portal_metadata=False if {url} is not a portal resource" + ) from err + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{meta_url} reported rowsUpdatedAt {value!r}, not epoch seconds") + return value + + +def pin( + url: str, + *, + licence: str | None = None, + dataset_id: str | None = None, + portal_metadata: bool | None = None, + tool: str = PIN_TOOL, + save: str | os.PathLike[str] | None = None, + client: httpx.Client | None = None, + transport: httpx.BaseTransport | None = None, + clock: Callable[[], datetime] | None = None, + timeout: float = 60.0, +) -> Pinned: + """Fetch ``url`` and return ``Pinned(content, entry)``. + + ``content`` is the response body (after HTTP content decoding), so a notebook can read the + pinned bytes from memory, for example ``pd.read_csv(io.BytesIO(content))``. ``entry`` is a + retrieval entry for the record's ``queries[]``: ``tool``, ``operationType: "retrieve"``, and + ``arguments`` with ``url`` (as given), ``sha256`` (hex, of ``content``), ``bytes`` (its + length), ``httpStatus`` and ``fetchedAt`` (ISO 8601 UTC, when the response arrived); + ``finalUrl`` when redirects led elsewhere; ``licence`` when given. + + For a URL with a portal resource's shape (``portal_metadata=None``, the default, decides by + shape; ``True`` requires the shape; ``False`` skips it), a second request reads the portal's + ``rowsUpdatedAt``, which is added to ``arguments``, and the dataset id becomes ``datasetId``. + ``dataset_id`` sets ``datasetId`` for any URL. A response that is not 2xx raises + ``httpx.HTTPStatusError``. + + ``save`` writes the bytes to that path and the entry, as JSON, beside it at + ``.pin.json``. ``client`` (used as given, not closed) or ``transport`` (for a client + this call makes and closes) and ``clock`` (returns an aware ``datetime``) are for tests and + for callers with their own HTTP settings. + """ + import httpx + + portal_id = portal_dataset_id(url) + if portal_metadata is True and portal_id is None: + raise ValueError( + f"{url} does not have a portal resource's shape (…/resource/ or …/api/views//rows.)" + ) + read_portal = portal_id is not None and portal_metadata is not False + now = clock or (lambda: datetime.now(UTC)) + + own = client is None + http = client if client is not None else httpx.Client(transport=transport, timeout=timeout, follow_redirects=True) + try: + response = http.get(url) + fetched_at = _utc(now()) + response.raise_for_status() + content = response.content + rows_updated_at = _rows_updated_at(http, url, portal_id) if read_portal and portal_id else None + finally: + if own: + http.close() + + arguments: dict[str, Any] = { + "url": url, + "sha256": hashlib.sha256(content).hexdigest(), + "bytes": len(content), + "httpStatus": response.status_code, + "fetchedAt": fetched_at, + } + if response.history: + arguments["finalUrl"] = str(response.url) + if licence is not None: + arguments["licence"] = licence + if rows_updated_at is not None: + arguments["rowsUpdatedAt"] = rows_updated_at + entry: dict[str, Any] = {"tool": tool, "operationType": "retrieve", "arguments": arguments} + if dataset_id is not None or read_portal: + entry["datasetId"] = dataset_id if dataset_id is not None else portal_id + + if save is not None: + target = Path(save) + target.write_bytes(content) + Path(f"{target}.pin.json").write_text(json.dumps(entry, indent=1, ensure_ascii=False) + "\n", encoding="utf-8") + return Pinned(content, entry) diff --git a/tests/conftest.py b/tests/conftest.py index 62adf99..3ec26af 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,42 @@ -"""Fixtures: a test seed in the environment, and a recorder of the wrapper's child processes.""" +"""Fixtures: no network for any test, a test seed in the environment, and a recorder of the +wrapper's child processes.""" from __future__ import annotations import os +import socket import subprocess from typing import Any import pytest -from support import SEED_VARIABLE, fresh_seed_b64 +from support import SEED_VARIABLE, NetworkBlocked, fresh_seed_b64 + + +@pytest.fixture(autouse=True) +def no_network(monkeypatch: pytest.MonkeyPatch) -> None: + """Every test runs offline: opening a connection or binding a port raises NetworkBlocked. + + The CLI makes no network request and the helpers' HTTP goes through an injected + ``httpx.MockTransport``, so nothing a test does should reach a socket. Child processes (the + CLI under Node) are outside this guard; the CLI's own contract is that it opens none. + """ + + def connect(self: socket.socket, address: Any) -> None: + raise NetworkBlocked(f"a test opened a network connection to {address!r}") + + def connect_ex(self: socket.socket, address: Any) -> int: + raise NetworkBlocked(f"a test opened a network connection to {address!r}") + + def bind(self: socket.socket, address: Any) -> None: + raise NetworkBlocked(f"a test bound a port at {address!r}") + + def create_connection(address: Any, *args: Any, **kwargs: Any) -> socket.socket: + raise NetworkBlocked(f"a test opened a network connection to {address!r}") + + monkeypatch.setattr(socket.socket, "connect", connect) + monkeypatch.setattr(socket.socket, "connect_ex", connect_ex) + monkeypatch.setattr(socket.socket, "bind", bind) + monkeypatch.setattr(socket, "create_connection", create_connection) @pytest.fixture diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 77c6c49..8d46096 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -19,3 +19,58 @@ Re-derive either copy with `git -C show : > tests/fix - **`first-note.bundle.json`** is the bundle the host template serves, written by `@typedstandards/host-core` 0.1.1 with a top-level `trustRegistry`. `tests/test_d9.py` verifies it through the wrapper, which drops that key before the CLI sees it (typedstandards#136). + +## Captured outputs + +These files are not copies of another repository's file: each is the output of a stated command, +captured once and committed. A test pins each one's SHA-256 too. + +| File | Captured from | Command | Date | SHA-256 | Pinned by | +|---|---|---|---|---|---| +| `badge-golden.json` | `@typedstandards/host-core`'s `links.ts` at typedstandards `116882a` (`packages/host-core/src/links.ts`), run by Node 24.21.0 | below | 2026-10-03 | `bd70f819249e8f8a5f3cf1245cbd521c32e3671623a4d3d6e009f71ae6661eaa` | `tests/test_badge.py` | +| `record-active.signed.json` | the wrapper's `sign`, through the vendored `@typedstandards/cli` 0.2.0, Node 24.21.0 | `capture_records.py` | 2026-10-03 | `6a43fcbbd205a94ad71b94c7600b61c740177568b345af2744a947236285abfc` | `tests/test_fixtures.py` | +| `record-active.bundle.json` | the wrapper's `view` of the above | `capture_records.py` | 2026-10-03 | `aa75bd9ad6f7c101ee2c3a96373e97826e6fae6dfa172bdc1e84bf69efcf5937` | `tests/test_fixtures.py` | +| `record-active.verify.json` | the wrapper's `verify` (`--json`) of the bundle | `capture_records.py` | 2026-10-03 | `a37890a4413b0ff206b0fc19f0cc998732c6b9590d504d5639b7ebbf3fccc833` | `tests/test_fixtures.py` | +| `record-withdrawn.signed.json` | the wrapper's `sign`, as above | `capture_records.py` | 2026-10-03 | `cbc91d561707f1e10034ff01a5cd587fb52276bae79f626665b14aa8d4bf5a9b` | `tests/test_fixtures.py` | +| `record-withdrawn.withdrawal.json` | the wrapper's `withdraw` of that record, same key | `capture_records.py` | 2026-10-03 | `42bfe9b0ee86ba5b219c62a472a1d0e8ee45b5191fc7a7dd8cde842b69859a2e` | `tests/test_fixtures.py` | +| `record-withdrawn.bundle.json` | the wrapper's `view` of the record with the withdrawal | `capture_records.py` | 2026-10-03 | `33b608d24e36beca3314fde938513252380e2ffbeaff3312e4dcb4f58482d2eb` | `tests/test_fixtures.py` | +| `record-withdrawn.verify.json` | the wrapper's `verify` (`--json`) of the bundle | `capture_records.py` | 2026-10-03 | `8b882a0bc06300add88b5186b11861279a1f6505f80344d1c279aa1eaa3ad4ef` | `tests/test_fixtures.py` | +| `show-active.html` | `typedstandards.show` of `record-active.bundle.json` with `record-active.verify.json`, plus a newline | below | 2026-10-03 | `d3a7d57e3cd0b43c64df3b09739266dcf904d2aba57d8aeb9653933998e56adc` | `tests/test_show.py` (byte-equal) | +| `show-withdrawn.html` | the same for the withdrawn record | below | 2026-10-03 | `0dac25d3c0d124c5c158d9678aec0ff4f316aaaa13d93555a942c87bbd106f8d` | `tests/test_show.py` (byte-equal) | + +- **`badge-golden.json`** holds six bundle URLs, each with what host-core's own + `buildVerifyHref(CANONICAL_ORIGIN, url)` and `buildEmbedMarkdown(CANONICAL_ORIGIN, url)` + return for it. The URLs cover the characters `encodeURIComponent` leaves unescaped, a space and + query characters, non-ASCII text and a literal `%`, and the characters it must escape. The first + is the host template's served bundle, whose Markdown is the template's README line 9 at + `70bfd18`. `tests/test_badge.py` asserts the wrapper's link and Markdown equal each case byte for + byte. To re-capture: write `links.ts` out with + `git -C show 116882a:packages/host-core/src/links.ts > links.ts`, then run with + Node 24 (which strips the file's types) a module that imports `buildVerifyHref`, + `buildEmbedMarkdown` and `CANONICAL_ORIGIN` from `./links.ts`, maps the six URLs to + `{url, verify, markdown}`, and prints `JSON.stringify({ cases }, null, 1)` and a newline. + +- **`record-*.json`** are two records signed through the wrapper under a throwaway seed, generated + for the capture and never stored: an active analysis notebook (a synthetic notebook with the + badge cell and the comparison cell, role `analysis`) and a claim (role `claim`), withdrawn by + the same key with a stated reason. Each carries a `vcsRef` to a fictional repository and its + role under `extensions`. The command, run from the repository root on 2026-10-03 with Node + 24.21.0 and the vendored CLI 0.2.0: + + ```sh + TYPEDSTANDARDS_SIGNING_SEED_B64="$(openssl rand -base64 32)" uv run python tests/fixtures/capture_records.py + ``` + + A new run signs with a new key, so it writes different signatures, `did:key`s and hashes, and + the pinned SHA-256s must be updated with it. The documents carry the throwaway key's public + `did:key`, which `.gitleaks.toml` allows. `tests/test_fixtures.py` checks that the vendored CLI's + `verify --json` of each bundle still equals the captured document; `tests/test_sidecar.py` builds + a fresh `view` from each `*.signed.json` through the CLI; `tests/test_show.py` renders from the + captured bundles and `verify` documents without Node. + +- **`show-*.html`** are this package's own rendering of the two captured records, committed so + that a change to `show`'s output, intended or not, fails `tests/test_show.py`, which asserts the + rendering equals the file byte for byte. They are written by + `sys.stdout.buffer.write((typedstandards.show(bundle, result).html + "\n").encode())` with each + record's bundle and verify document loaded from this directory. Regenerate them only with a + deliberate change to the rendering. diff --git a/tests/fixtures/badge-golden.json b/tests/fixtures/badge-golden.json new file mode 100644 index 0000000..32bdebe --- /dev/null +++ b/tests/fixtures/badge-golden.json @@ -0,0 +1,34 @@ +{ + "cases": [ + { + "url": "https://host-template.typedstandards.org/bundles/first-note.bundle.json", + "verify": "https://typedstandards.org/verify?url=https%3A%2F%2Fhost-template.typedstandards.org%2Fbundles%2Ffirst-note.bundle.json", + "markdown": "[![Verify this record with Typed Standards](https://typedstandards.org/badge/typed-standards-verify.svg)]()" + }, + { + "url": "https://records.example.org/bundles/analysis.bundle.json", + "verify": "https://typedstandards.org/verify?url=https%3A%2F%2Frecords.example.org%2Fbundles%2Fanalysis.bundle.json", + "markdown": "[![Verify this record with Typed Standards](https://typedstandards.org/badge/typed-standards-verify.svg)]()" + }, + { + "url": "https://records.example.org/a-b_c.d!e~f*g'h(i)j/k.bundle.json", + "verify": "https://typedstandards.org/verify?url=https%3A%2F%2Frecords.example.org%2Fa-b_c.d!e~f*g'h(i)j%2Fk.bundle.json", + "markdown": "[![Verify this record with Typed Standards](https://typedstandards.org/badge/typed-standards-verify.svg)]()" + }, + { + "url": "https://records.example.org/path with space/x.bundle.json?q=1&r=a+b#frag", + "verify": "https://typedstandards.org/verify?url=https%3A%2F%2Frecords.example.org%2Fpath%20with%20space%2Fx.bundle.json%3Fq%3D1%26r%3Da%2Bb%23frag", + "markdown": "[![Verify this record with Typed Standards](https://typedstandards.org/badge/typed-standards-verify.svg)]()" + }, + { + "url": "https://records.example.org/café/実験/%41.bundle.json", + "verify": "https://typedstandards.org/verify?url=https%3A%2F%2Frecords.example.org%2Fcaf%C3%A9%2F%E5%AE%9F%E9%A8%93%2F%2541.bundle.json", + "markdown": "[![Verify this record with Typed Standards](https://typedstandards.org/badge/typed-standards-verify.svg)]()" + }, + { + "url": "https://records.example.org//\"quoted\"/[x]{y}|z^`\\.bundle.json", + "verify": "https://typedstandards.org/verify?url=https%3A%2F%2Frecords.example.org%2F%3Ctag%3E%2F%22quoted%22%2F%5Bx%5D%7By%7D%7Cz%5E%60%5C.bundle.json", + "markdown": "[![Verify this record with Typed Standards](https://typedstandards.org/badge/typed-standards-verify.svg)]()" + } + ] +} diff --git a/tests/fixtures/capture_records.py b/tests/fixtures/capture_records.py new file mode 100644 index 0000000..44fe7ea --- /dev/null +++ b/tests/fixtures/capture_records.py @@ -0,0 +1,100 @@ +"""Capture the record fixtures that ``show`` and ``sidecar`` are tested against. + +Run once, from the repository root, with a throwaway seed that is never stored: + + TYPEDSTANDARDS_SIGNING_SEED_B64="$(openssl rand -base64 32)" uv run python tests/fixtures/capture_records.py + +It signs two records through the wrapper (so through the vendored CLI): an active analysis +notebook and a claim that is then withdrawn, each with a ``vcsRef`` and a role under +``extensions``. For each it writes what ``sign`` printed, the bundle ``view`` printed, and what +``verify --json`` printed for that bundle; for the claim, also what ``withdraw`` printed. The +seed is read by the CLI from the environment this script inherits; the script never reads it. +""" + +from __future__ import annotations + +import json +import sys +import tempfile +from pathlib import Path + +HERE = Path(__file__).parent +sys.path.insert(0, str(HERE.parent)) + +from support import analysis_input, synthetic_notebook # noqa: E402 + +import typedstandards # noqa: E402 + +VCS = { + "repoUrl": "https://git.example.com/example/analysis", + "commitSha": "0123456789abcdef0123456789abcdef01234567", + "ref": "refs/heads/main", +} + + +def write(name: str, value: object) -> None: + (HERE / name).write_text(json.dumps(value, indent=1, ensure_ascii=False) + "\n", encoding="utf-8") + + +def main() -> None: + with tempfile.TemporaryDirectory() as tmp: + notebook = Path(tmp) / "analysis.ipynb" + notebook.write_text(synthetic_notebook(), encoding="utf-8") + typedstandards.badge_cell( + "https://records.example.org/bundles/analysis.bundle.json", capture_method="script-run", notebook=notebook + ) + typedstandards.comparison_cell( + notebook, {"total": 1.5}, recompute="recompute_key_metrics()", captured_at="2026-10-03T12:00:00Z" + ) + active = typedstandards.sign( + analysis_input( + packageId="6f9e2a54-6d1c-4f1e-9a52-3c7d1f0b8e21", + createdAt="2026-10-03T12:05:00.000Z", + summary="An example analysis notebook, executed end to end.", + extensions={"role": "analysis"}, + vcsRef={**VCS, "path": "notebooks/analysis.ipynb"}, + ), + output_file=notebook, + ) + + claim = Path(tmp) / "claim.md" + claim.write_text("The median fare rose by 4 percent between the two periods.\n", encoding="utf-8") + withdrawn = typedstandards.sign( + analysis_input( + packageId="0b3c8d7e-2f41-4a9b-8c65-91d2e4f7a3b0", + createdAt="2026-10-03T12:06:00.000Z", + summary="An example claim drawn from the analysis notebook.", + extensions={"role": "claim"}, + vcsRef={**VCS, "path": "claims/median-fare.md"}, + ), + output_file=claim, + ) + + withdrawal = typedstandards.withdraw( + { + "targetNodeId": withdrawn["envelopeHash"], + "reason": "The claim compared periods of different lengths; a restatement replaces it.", + "signer": withdrawn["package"]["signer"], + "packageId": "5a1d9c3e-7b20-4e8f-a6d4-2c9b0e1f3a57", + "createdAt": "2026-10-03T12:30:00.000Z", + "effectiveAt": "2026-10-03T12:30:00.000Z", + } + ) + + active_bundle = typedstandards.view(active, visibility="public", title="Example analysis") + withdrawn_bundle = typedstandards.view( + withdrawn, visibility="public", attestations=[withdrawal], title="Example claim" + ) + + write("record-active.signed.json", active) + write("record-active.bundle.json", active_bundle) + write("record-active.verify.json", typedstandards.verify(active_bundle)) + write("record-withdrawn.signed.json", withdrawn) + write("record-withdrawn.withdrawal.json", withdrawal) + write("record-withdrawn.bundle.json", withdrawn_bundle) + write("record-withdrawn.verify.json", typedstandards.verify(withdrawn_bundle)) + print(f"captured with @typedstandards/cli {typedstandards.cli_version()}") + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/record-active.bundle.json b/tests/fixtures/record-active.bundle.json new file mode 100644 index 0000000..952f1c6 --- /dev/null +++ b/tests/fixtures/record-active.bundle.json @@ -0,0 +1,79 @@ +{ + "protocolVersion": "0.1.0", + "packageHash": "a171d1ab156d578ecabae1722f5602f0fde77757e22aae07cfa67cbdf77fd963", + "visibility": "public", + "captureMethod": "script-run", + "contentProfile": "default", + "producerProfile": "scripted-recomputation/example", + "type": "content/analysis/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "contentHash": { + "sha256": "fa89d5ea2a270f75af92cc095c41c107bee2a2fbc8cb887b52bcc7e6e6a22645" + }, + "contentCanonicalization": "https://typedstandards.org/canonicalization/raw-bytes/v1", + "signature": { + "signature": "XvAJN/+6Ay0In3J6WtHC9WQXUUA+4PCUKERd0nzHb6+JxMHPFA/g30yfKzVeqqRK2hWn4LmCxF5rKJbg8nvyCQ==", + "publicKey": "MCowBQYDK2VwAyEA3qfVxXXRaXMS9un5GWm9HIHD8vKfhf3IV6GENrN/eKM=", + "algorithm": "Ed25519ph", + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "subjectTitle": "Example analysis", + "subjectSummary": "An example analysis notebook, executed end to end.", + "package": { + "metadata": { + "schemaVersion": "0.1.0", + "packageId": "6f9e2a54-6d1c-4f1e-9a52-3c7d1f0b8e21", + "createdAt": "2026-10-03T12:05:00.000Z", + "signingKeyId": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn", + "captureMethod": "script-run" + }, + "producerProfile": "scripted-recomputation/example", + "type": "content/analysis/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "vcsRef": { + "repoUrl": "https://git.example.com/example/analysis", + "commitSha": "0123456789abcdef0123456789abcdef01234567", + "ref": "refs/heads/main", + "path": "notebooks/analysis.ipynb" + }, + "contentCanonicalization": "https://typedstandards.org/canonicalization/raw-bytes/v1", + "prompt": { + "hash": "46da256c80af2a74594076f33694665b508d5c485abbfcc3f55030f247613226", + "visibility": "full_text", + "text": "Run the example notebook end to end and record its output." + }, + "queries": [ + { + "tool": "jupyter nbconvert --execute", + "operationType": "script-run", + "arguments": { + "notebook": "x" + } + } + ], + "dataSources": [], + "cost": { + "model": "none" + }, + "skillMetadata": {}, + "output": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"id\": \"typedstandards-badge\",\n \"metadata\": {},\n \"source\": [\n \"[![Verify this record with Typed Standards](https://typedstandards.org/badge/typed-standards-verify.svg)]()\\n\",\n \"\\n\",\n \"| Typed Standards record | |\\n\",\n \"|---|---|\\n\",\n \"| Host | `records.example.org` |\\n\",\n \"| Capture method | `script-run` |\\n\",\n \"\\n\",\n \"This cell is a reader affordance and is not authoritative: verification reads the signed record, not this cell, and the verifier shows the record's signer, hash and time.\\n\"\n ]\n },\n {\n \"cell_type\": \"markdown\",\n \"id\": \"intro\",\n \"metadata\": {},\n \"source\": [\n \"# Example analysis\\n\",\n \"\\n\",\n \"Résumé of the method — café 実験.\\n\"\n ]\n },\n {\n \"cell_type\": \"code\",\n \"execution_count\": 1,\n \"id\": \"load\",\n \"metadata\": {\n \"tags\": [\n \"parameters\"\n ]\n },\n \"outputs\": [\n {\n \"name\": \"stdout\",\n \"output_type\": \"stream\",\n \"text\": [\n \"1.50\\n\"\n ]\n }\n ],\n \"source\": [\n \"total = 1.50\\n\",\n \"print(f'{total:.2f}')\"\n ]\n },\n {\n \"cell_type\": \"code\",\n \"execution_count\": null,\n \"id\": \"plot\",\n \"metadata\": {},\n \"outputs\": [],\n \"source\": [\n \"values = [1e-3, 2.0, -0.0]\"\n ]\n },\n {\n \"cell_type\": \"code\",\n \"execution_count\": null,\n \"id\": \"typedstandards-comparison\",\n \"metadata\": {},\n \"outputs\": [],\n \"source\": [\n \"# ORIGINAL VALUES (captured at executedAt = 2026-10-03T12:00:00Z)\\n\",\n \"original = {\\n\",\n \" \\\"total\\\": 1.5,\\n\",\n \"}\\n\",\n \"\\n\",\n \"# CURRENT VALUES (re-computed against live data using the same helpers + queries above)\\n\",\n \"current = recompute_key_metrics()\\n\",\n \"\\n\",\n \"# DELTAS\\n\",\n \"for k in original:\\n\",\n \" delta = (current[k] - original[k]) if isinstance(original[k], (int, float)) else (original[k], current[k])\\n\",\n \" print(f\\\"{k}: original={original[k]}, current={current[k]}, delta={delta}\\\")\\n\"\n ]\n }\n ],\n \"metadata\": {\n \"kernelspec\": {\n \"display_name\": \"Python 3\",\n \"language\": \"python\",\n \"name\": \"python3\"\n },\n \"language_info\": {\n \"name\": \"python\",\n \"version\": \"3.12.0\"\n }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 5\n}\n", + "trace": { + "resourceSpans": [] + }, + "summary": "An example analysis notebook, executed end to end.", + "extensions": { + "role": "analysis" + }, + "contentHash": { + "sha256": "fa89d5ea2a270f75af92cc095c41c107bee2a2fbc8cb887b52bcc7e6e6a22645" + } + } +} diff --git a/tests/fixtures/record-active.signed.json b/tests/fixtures/record-active.signed.json new file mode 100644 index 0000000..0284c0e --- /dev/null +++ b/tests/fixtures/record-active.signed.json @@ -0,0 +1,62 @@ +{ + "package": { + "metadata": { + "schemaVersion": "0.1.0", + "packageId": "6f9e2a54-6d1c-4f1e-9a52-3c7d1f0b8e21", + "createdAt": "2026-10-03T12:05:00.000Z", + "signingKeyId": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn", + "captureMethod": "script-run" + }, + "producerProfile": "scripted-recomputation/example", + "type": "content/analysis/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "vcsRef": { + "repoUrl": "https://git.example.com/example/analysis", + "commitSha": "0123456789abcdef0123456789abcdef01234567", + "ref": "refs/heads/main", + "path": "notebooks/analysis.ipynb" + }, + "contentCanonicalization": "https://typedstandards.org/canonicalization/raw-bytes/v1", + "prompt": { + "hash": "46da256c80af2a74594076f33694665b508d5c485abbfcc3f55030f247613226", + "visibility": "full_text", + "text": "Run the example notebook end to end and record its output." + }, + "queries": [ + { + "tool": "jupyter nbconvert --execute", + "operationType": "script-run", + "arguments": { + "notebook": "x" + } + } + ], + "dataSources": [], + "cost": { + "model": "none" + }, + "skillMetadata": {}, + "output": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"id\": \"typedstandards-badge\",\n \"metadata\": {},\n \"source\": [\n \"[![Verify this record with Typed Standards](https://typedstandards.org/badge/typed-standards-verify.svg)]()\\n\",\n \"\\n\",\n \"| Typed Standards record | |\\n\",\n \"|---|---|\\n\",\n \"| Host | `records.example.org` |\\n\",\n \"| Capture method | `script-run` |\\n\",\n \"\\n\",\n \"This cell is a reader affordance and is not authoritative: verification reads the signed record, not this cell, and the verifier shows the record's signer, hash and time.\\n\"\n ]\n },\n {\n \"cell_type\": \"markdown\",\n \"id\": \"intro\",\n \"metadata\": {},\n \"source\": [\n \"# Example analysis\\n\",\n \"\\n\",\n \"Résumé of the method — café 実験.\\n\"\n ]\n },\n {\n \"cell_type\": \"code\",\n \"execution_count\": 1,\n \"id\": \"load\",\n \"metadata\": {\n \"tags\": [\n \"parameters\"\n ]\n },\n \"outputs\": [\n {\n \"name\": \"stdout\",\n \"output_type\": \"stream\",\n \"text\": [\n \"1.50\\n\"\n ]\n }\n ],\n \"source\": [\n \"total = 1.50\\n\",\n \"print(f'{total:.2f}')\"\n ]\n },\n {\n \"cell_type\": \"code\",\n \"execution_count\": null,\n \"id\": \"plot\",\n \"metadata\": {},\n \"outputs\": [],\n \"source\": [\n \"values = [1e-3, 2.0, -0.0]\"\n ]\n },\n {\n \"cell_type\": \"code\",\n \"execution_count\": null,\n \"id\": \"typedstandards-comparison\",\n \"metadata\": {},\n \"outputs\": [],\n \"source\": [\n \"# ORIGINAL VALUES (captured at executedAt = 2026-10-03T12:00:00Z)\\n\",\n \"original = {\\n\",\n \" \\\"total\\\": 1.5,\\n\",\n \"}\\n\",\n \"\\n\",\n \"# CURRENT VALUES (re-computed against live data using the same helpers + queries above)\\n\",\n \"current = recompute_key_metrics()\\n\",\n \"\\n\",\n \"# DELTAS\\n\",\n \"for k in original:\\n\",\n \" delta = (current[k] - original[k]) if isinstance(original[k], (int, float)) else (original[k], current[k])\\n\",\n \" print(f\\\"{k}: original={original[k]}, current={current[k]}, delta={delta}\\\")\\n\"\n ]\n }\n ],\n \"metadata\": {\n \"kernelspec\": {\n \"display_name\": \"Python 3\",\n \"language\": \"python\",\n \"name\": \"python3\"\n },\n \"language_info\": {\n \"name\": \"python\",\n \"version\": \"3.12.0\"\n }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 5\n}\n", + "trace": { + "resourceSpans": [] + }, + "summary": "An example analysis notebook, executed end to end.", + "extensions": { + "role": "analysis" + }, + "contentHash": { + "sha256": "fa89d5ea2a270f75af92cc095c41c107bee2a2fbc8cb887b52bcc7e6e6a22645" + } + }, + "envelopeHash": "a171d1ab156d578ecabae1722f5602f0fde77757e22aae07cfa67cbdf77fd963", + "signature": { + "signature": "XvAJN/+6Ay0In3J6WtHC9WQXUUA+4PCUKERd0nzHb6+JxMHPFA/g30yfKzVeqqRK2hWn4LmCxF5rKJbg8nvyCQ==", + "publicKey": "MCowBQYDK2VwAyEA3qfVxXXRaXMS9un5GWm9HIHD8vKfhf3IV6GENrN/eKM=", + "algorithm": "Ed25519ph", + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + } +} diff --git a/tests/fixtures/record-active.verify.json b/tests/fixtures/record-active.verify.json new file mode 100644 index 0000000..27b19d7 --- /dev/null +++ b/tests/fixtures/record-active.verify.json @@ -0,0 +1,74 @@ +{ + "ok": true, + "nodeId": "a171d1ab156d578ecabae1722f5602f0fde77757e22aae07cfa67cbdf77fd963", + "failures": [], + "checks": { + "hashMatch": true, + "envelopeIntegrity": { + "status": "verified" + }, + "recomputedHash": "a171d1ab156d578ecabae1722f5602f0fde77757e22aae07cfa67cbdf77fd963", + "nodeId": "a171d1ab156d578ecabae1722f5602f0fde77757e22aae07cfa67cbdf77fd963", + "signatureValid": true, + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn", + "hasSigning": true, + "rekorVerified": null, + "rekorDetails": null, + "rekorInclusion": null, + "hasRekor": false, + "hasTimestamp": false, + "rfc3161": null, + "keyTrust": { + "status": "self_certified", + "verified": false, + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "blobRefsVerified": null, + "blobRefs": [], + "contentCanonicalization": { + "status": "ok", + "rule": "https://typedstandards.org/canonicalization/raw-bytes/v1" + }, + "contentHash": { + "status": "ok", + "algorithms": [ + "sha256" + ], + "matched": "sha256", + "contentHash": { + "sha256": "fa89d5ea2a270f75af92cc095c41c107bee2a2fbc8cb887b52bcc7e6e6a22645" + } + }, + "typeResolution": { + "status": "ok", + "type": "content/analysis/v1" + }, + "signerIdentity": { + "status": "key_derived_match", + "claimed": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn", + "derived": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "signingKeyIdConsistency": { + "status": "ok" + }, + "captureMethodVocab": { + "status": "ok", + "captureMethod": "script-run", + "profileType": "scripted-recomputation" + }, + "contentProfile": { + "status": "contentProfile_absent", + "producerProfile": "scripted-recomputation/example" + }, + "lifecycle": { + "status": "active", + "source": "none", + "chain": [] + } + }, + "lifecycle": { + "status": "active", + "source": "none", + "chain": [] + } +} diff --git a/tests/fixtures/record-withdrawn.bundle.json b/tests/fixtures/record-withdrawn.bundle.json new file mode 100644 index 0000000..699daf6 --- /dev/null +++ b/tests/fixtures/record-withdrawn.bundle.json @@ -0,0 +1,116 @@ +{ + "protocolVersion": "0.1.0", + "packageHash": "cba7829c5b6374f760b9558227e2a9faccfdb457d104016d95a5acc4923423ab", + "visibility": "public", + "captureMethod": "script-run", + "contentProfile": "default", + "producerProfile": "scripted-recomputation/example", + "type": "content/analysis/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "contentHash": { + "sha256": "5e1d84bd5e11836479fc3749bd8e167310e1199dbd65bf64b5e6223b1dbc8522" + }, + "contentCanonicalization": "https://typedstandards.org/canonicalization/raw-bytes/v1", + "signature": { + "signature": "xXfyPAhztpEcaRf8Hs6WZZI1yqsdCrRkGagWw1FO6ZHk2FG7tL5RPttaCjQhWGofAZYdvho6C7r+aYRACH5FBg==", + "publicKey": "MCowBQYDK2VwAyEA3qfVxXXRaXMS9un5GWm9HIHD8vKfhf3IV6GENrN/eKM=", + "algorithm": "Ed25519ph", + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "lifecycle": { + "status": "withdrawn", + "withdrawnAt": "2026-10-03T12:30:00.000Z", + "withdrawnReason": "The claim compared periods of different lengths; a restatement replaces it." + }, + "lifecycleAttestations": [ + { + "node": { + "metadata": { + "schemaVersion": "0.1.0", + "packageId": "5a1d9c3e-7b20-4e8f-a6d4-2c9b0e1f3a57", + "createdAt": "2026-10-03T12:30:00.000Z", + "signingKeyId": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "type": "attestation/withdraws/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "targetNodeId": "cba7829c5b6374f760b9558227e2a9faccfdb457d104016d95a5acc4923423ab", + "contentCanonicalization": "https://typedstandards.org/canonicalization/legacy-json/v1", + "reason": "The claim compared periods of different lengths; a restatement replaces it.", + "effectiveAt": "2026-10-03T12:30:00.000Z", + "contentHash": { + "sha256": "412e4124e1a61a2e259c5350a8efecd0b9b851a8eaa0925654884ef75cf66174" + } + }, + "nodeId": "f9c3bcea58a9407f453a5b102283f8eb79bb643bbbdb731648c7a2d7cec60044", + "signature": { + "signature": "cGDO7H8AiyvdudFeiBTj4EsMKOQshCK9tRnhvORFKr1HBoIcyqN8cTQfHQ27+iSga/tTv3uvO2ombi0xBKXLAw==", + "publicKey": "MCowBQYDK2VwAyEA3qfVxXXRaXMS9un5GWm9HIHD8vKfhf3IV6GENrN/eKM=", + "algorithm": "Ed25519ph", + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + } + } + ], + "subjectTitle": "Example claim", + "subjectSummary": "An example claim drawn from the analysis notebook.", + "package": { + "metadata": { + "schemaVersion": "0.1.0", + "packageId": "0b3c8d7e-2f41-4a9b-8c65-91d2e4f7a3b0", + "createdAt": "2026-10-03T12:06:00.000Z", + "signingKeyId": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn", + "captureMethod": "script-run" + }, + "producerProfile": "scripted-recomputation/example", + "type": "content/analysis/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "vcsRef": { + "repoUrl": "https://git.example.com/example/analysis", + "commitSha": "0123456789abcdef0123456789abcdef01234567", + "ref": "refs/heads/main", + "path": "claims/median-fare.md" + }, + "contentCanonicalization": "https://typedstandards.org/canonicalization/raw-bytes/v1", + "prompt": { + "hash": "46da256c80af2a74594076f33694665b508d5c485abbfcc3f55030f247613226", + "visibility": "full_text", + "text": "Run the example notebook end to end and record its output." + }, + "queries": [ + { + "tool": "jupyter nbconvert --execute", + "operationType": "script-run", + "arguments": { + "notebook": "x" + } + } + ], + "dataSources": [], + "cost": { + "model": "none" + }, + "skillMetadata": {}, + "output": "The median fare rose by 4 percent between the two periods.\n", + "trace": { + "resourceSpans": [] + }, + "summary": "An example claim drawn from the analysis notebook.", + "extensions": { + "role": "claim" + }, + "contentHash": { + "sha256": "5e1d84bd5e11836479fc3749bd8e167310e1199dbd65bf64b5e6223b1dbc8522" + } + } +} diff --git a/tests/fixtures/record-withdrawn.signed.json b/tests/fixtures/record-withdrawn.signed.json new file mode 100644 index 0000000..df10f1a --- /dev/null +++ b/tests/fixtures/record-withdrawn.signed.json @@ -0,0 +1,62 @@ +{ + "package": { + "metadata": { + "schemaVersion": "0.1.0", + "packageId": "0b3c8d7e-2f41-4a9b-8c65-91d2e4f7a3b0", + "createdAt": "2026-10-03T12:06:00.000Z", + "signingKeyId": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn", + "captureMethod": "script-run" + }, + "producerProfile": "scripted-recomputation/example", + "type": "content/analysis/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "vcsRef": { + "repoUrl": "https://git.example.com/example/analysis", + "commitSha": "0123456789abcdef0123456789abcdef01234567", + "ref": "refs/heads/main", + "path": "claims/median-fare.md" + }, + "contentCanonicalization": "https://typedstandards.org/canonicalization/raw-bytes/v1", + "prompt": { + "hash": "46da256c80af2a74594076f33694665b508d5c485abbfcc3f55030f247613226", + "visibility": "full_text", + "text": "Run the example notebook end to end and record its output." + }, + "queries": [ + { + "tool": "jupyter nbconvert --execute", + "operationType": "script-run", + "arguments": { + "notebook": "x" + } + } + ], + "dataSources": [], + "cost": { + "model": "none" + }, + "skillMetadata": {}, + "output": "The median fare rose by 4 percent between the two periods.\n", + "trace": { + "resourceSpans": [] + }, + "summary": "An example claim drawn from the analysis notebook.", + "extensions": { + "role": "claim" + }, + "contentHash": { + "sha256": "5e1d84bd5e11836479fc3749bd8e167310e1199dbd65bf64b5e6223b1dbc8522" + } + }, + "envelopeHash": "cba7829c5b6374f760b9558227e2a9faccfdb457d104016d95a5acc4923423ab", + "signature": { + "signature": "xXfyPAhztpEcaRf8Hs6WZZI1yqsdCrRkGagWw1FO6ZHk2FG7tL5RPttaCjQhWGofAZYdvho6C7r+aYRACH5FBg==", + "publicKey": "MCowBQYDK2VwAyEA3qfVxXXRaXMS9un5GWm9HIHD8vKfhf3IV6GENrN/eKM=", + "algorithm": "Ed25519ph", + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + } +} diff --git a/tests/fixtures/record-withdrawn.verify.json b/tests/fixtures/record-withdrawn.verify.json new file mode 100644 index 0000000..f0efeb7 --- /dev/null +++ b/tests/fixtures/record-withdrawn.verify.json @@ -0,0 +1,120 @@ +{ + "ok": true, + "nodeId": "cba7829c5b6374f760b9558227e2a9faccfdb457d104016d95a5acc4923423ab", + "failures": [], + "checks": { + "hashMatch": true, + "envelopeIntegrity": { + "status": "verified" + }, + "recomputedHash": "cba7829c5b6374f760b9558227e2a9faccfdb457d104016d95a5acc4923423ab", + "nodeId": "cba7829c5b6374f760b9558227e2a9faccfdb457d104016d95a5acc4923423ab", + "signatureValid": true, + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn", + "hasSigning": true, + "rekorVerified": null, + "rekorDetails": null, + "rekorInclusion": null, + "hasRekor": false, + "hasTimestamp": false, + "rfc3161": null, + "keyTrust": { + "status": "self_certified", + "verified": false, + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "blobRefsVerified": null, + "blobRefs": [], + "contentCanonicalization": { + "status": "ok", + "rule": "https://typedstandards.org/canonicalization/raw-bytes/v1" + }, + "contentHash": { + "status": "ok", + "algorithms": [ + "sha256" + ], + "matched": "sha256", + "contentHash": { + "sha256": "5e1d84bd5e11836479fc3749bd8e167310e1199dbd65bf64b5e6223b1dbc8522" + } + }, + "typeResolution": { + "status": "ok", + "type": "content/analysis/v1" + }, + "signerIdentity": { + "status": "key_derived_match", + "claimed": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn", + "derived": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "signingKeyIdConsistency": { + "status": "ok" + }, + "captureMethodVocab": { + "status": "ok", + "captureMethod": "script-run", + "profileType": "scripted-recomputation" + }, + "contentProfile": { + "status": "contentProfile_absent", + "producerProfile": "scripted-recomputation/example" + }, + "lifecycle": { + "status": "withdrawn", + "source": "attestation-chain", + "chain": [ + { + "nodeId": "f9c3bcea58a9407f453a5b102283f8eb79bb643bbbdb731648c7a2d7cec60044", + "type": "attestation/withdraws/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "createdAt": "2026-10-03T12:30:00.000Z", + "reason": "The claim compared periods of different lengths; a restatement replaces it.", + "effectiveAt": "2026-10-03T12:30:00.000Z", + "signatureValid": true, + "nodeIdMatches": true, + "hasTimestamp": false, + "hasRekor": false, + "signerMatchesTarget": true, + "keyBound": true, + "namesTarget": true, + "missingFields": [] + } + ], + "withdrawnAt": "2026-10-03T12:30:00.000Z", + "withdrawnReason": "The claim compared periods of different lengths; a restatement replaces it." + } + }, + "lifecycle": { + "status": "withdrawn", + "source": "attestation-chain", + "chain": [ + { + "nodeId": "f9c3bcea58a9407f453a5b102283f8eb79bb643bbbdb731648c7a2d7cec60044", + "type": "attestation/withdraws/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "createdAt": "2026-10-03T12:30:00.000Z", + "reason": "The claim compared periods of different lengths; a restatement replaces it.", + "effectiveAt": "2026-10-03T12:30:00.000Z", + "signatureValid": true, + "nodeIdMatches": true, + "hasTimestamp": false, + "hasRekor": false, + "signerMatchesTarget": true, + "keyBound": true, + "namesTarget": true, + "missingFields": [] + } + ], + "withdrawnAt": "2026-10-03T12:30:00.000Z", + "withdrawnReason": "The claim compared periods of different lengths; a restatement replaces it." + } +} diff --git a/tests/fixtures/record-withdrawn.withdrawal.json b/tests/fixtures/record-withdrawn.withdrawal.json new file mode 100644 index 0000000..d97f36a --- /dev/null +++ b/tests/fixtures/record-withdrawn.withdrawal.json @@ -0,0 +1,30 @@ +{ + "node": { + "metadata": { + "schemaVersion": "0.1.0", + "packageId": "5a1d9c3e-7b20-4e8f-a6d4-2c9b0e1f3a57", + "createdAt": "2026-10-03T12:30:00.000Z", + "signingKeyId": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "type": "attestation/withdraws/v1", + "signer": { + "bindingTier": "pseudonymous", + "displayName": "Example analyst", + "identifier": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + }, + "targetNodeId": "cba7829c5b6374f760b9558227e2a9faccfdb457d104016d95a5acc4923423ab", + "contentCanonicalization": "https://typedstandards.org/canonicalization/legacy-json/v1", + "reason": "The claim compared periods of different lengths; a restatement replaces it.", + "effectiveAt": "2026-10-03T12:30:00.000Z", + "contentHash": { + "sha256": "412e4124e1a61a2e259c5350a8efecd0b9b851a8eaa0925654884ef75cf66174" + } + }, + "nodeId": "f9c3bcea58a9407f453a5b102283f8eb79bb643bbbdb731648c7a2d7cec60044", + "signature": { + "signature": "cGDO7H8AiyvdudFeiBTj4EsMKOQshCK9tRnhvORFKr1HBoIcyqN8cTQfHQ27+iSga/tTv3uvO2ombi0xBKXLAw==", + "publicKey": "MCowBQYDK2VwAyEA3qfVxXXRaXMS9un5GWm9HIHD8vKfhf3IV6GENrN/eKM=", + "algorithm": "Ed25519ph", + "kid": "did:key:z6MkuSQzHivrroMKjau3BrVNaK3cGQq9RdUiXuMH7YAwYNzn" + } +} diff --git a/tests/fixtures/show-active.html b/tests/fixtures/show-active.html new file mode 100644 index 0000000..21ba5ef --- /dev/null +++ b/tests/fixtures/show-active.html @@ -0,0 +1 @@ +

    Typed Standards record: Example analysis

    Typecontent/analysis/v1
    Roleanalysis (asserted by the signer in extensions["role"]; not checked)
    Statusactive
    Signerdid:key:z6MkuSQz…AwYNzn · Signed with a self-certifying key
    The signer's identifier is derived from the signing key, so it proves that the same key signed everything under this identifier, not who holds the key. No registry vouches for it, and the key cannot be rotated or revoked.
    Namecalls itself “Example analyst” (the signer’s own description; not verified)
    Binding tierdescribes itself as pseudonymous (the signer’s own description; no check establishes it)
    Envelope hasha171d1ab156d…
    Created2026-10-03T12:05:00.000Z (the signer’s clock)
    Source revisionhttps://git.example.com/example/analysis, commit 0123456789abcdef0123456789abcdef01234567, path notebooks/analysis.ipynb, ref refs/heads/main (asserted; not fetched)
    SummaryAn example analysis notebook, executed end to end. (the signer’s)

    verify: ok, no check contradicts the record

    • #1 Envelope integrity: verified
    • #2 Signature: valid
    • #3 Content canonicalization: ok, https://typedstandards.org/canonicalization/raw-bytes/v1
    • #4 Content hash: ok, sha256
    • #5 Key trust: Signed with a self-certifying key (self_certified; no registry vouches for it)
    • #6 Signing key id: ok
    • #7 Timestamp: no RFC 3161 token carried
    • #8 Transparency log: no log entry carried
    • #9 Referenced files: none
    • #10 Lifecycle: active (source: none)
    • #11 Capture method: script-run
    • #12 Type: ok, content/analysis/v1
    • #14 Signer identity: Signer identifier matches the signing key (key_derived_match)
    • #15 Capture method vocabulary: ok, scripted-recomputation
    • #16 Content profile: contentProfile_absent

    This shows that the record is intact and which key signed it; it does not say the analysis is correct.

    diff --git a/tests/fixtures/show-withdrawn.html b/tests/fixtures/show-withdrawn.html new file mode 100644 index 0000000..16d249d --- /dev/null +++ b/tests/fixtures/show-withdrawn.html @@ -0,0 +1 @@ +

    Typed Standards record: Example claim

    Typecontent/analysis/v1
    Roleclaim (asserted by the signer in extensions["role"]; not checked)
    Statuswithdrawn, reason: The claim compared periods of different lengths; a restatement replaces it. (at 2026-10-03T12:30:00.000Z)
    Signerdid:key:z6MkuSQz…AwYNzn · Signed with a self-certifying key
    The signer's identifier is derived from the signing key, so it proves that the same key signed everything under this identifier, not who holds the key. No registry vouches for it, and the key cannot be rotated or revoked.
    Namecalls itself “Example analyst” (the signer’s own description; not verified)
    Binding tierdescribes itself as pseudonymous (the signer’s own description; no check establishes it)
    Envelope hashcba7829c5b63…
    Created2026-10-03T12:06:00.000Z (the signer’s clock)
    Source revisionhttps://git.example.com/example/analysis, commit 0123456789abcdef0123456789abcdef01234567, path claims/median-fare.md, ref refs/heads/main (asserted; not fetched)
    SummaryAn example claim drawn from the analysis notebook. (the signer’s)

    verify: ok, no check contradicts the record

    • #1 Envelope integrity: verified
    • #2 Signature: valid
    • #3 Content canonicalization: ok, https://typedstandards.org/canonicalization/raw-bytes/v1
    • #4 Content hash: ok, sha256
    • #5 Key trust: Signed with a self-certifying key (self_certified; no registry vouches for it)
    • #6 Signing key id: ok
    • #7 Timestamp: no RFC 3161 token carried
    • #8 Transparency log: no log entry carried
    • #9 Referenced files: none
    • #10 Lifecycle: withdrawn (source: attestation-chain)
    • #11 Capture method: script-run
    • #12 Type: ok, content/analysis/v1
    • #14 Signer identity: Signer identifier matches the signing key (key_derived_match)
    • #15 Capture method vocabulary: ok, scripted-recomputation
    • #16 Content profile: contentProfile_absent

    This shows that the record is intact and which key signed it; it does not say the analysis is correct.

    diff --git a/tests/guards.py b/tests/guards.py index 9cc0801..3df584b 100644 --- a/tests/guards.py +++ b/tests/guards.py @@ -23,8 +23,8 @@ {"hashlib", "_hashlib", "_sha1", "_sha2", "_sha256", "_sha512", "_sha3", "_md5", "_blake2", "hmac"} ) -#: The one module allowed to import hashlib: P2's pin (it does not exist yet), relative to the -#: package directory. +#: The one module allowed to import hashlib, relative to the package directory: pin.py, whose +#: SHA-256 of fetched bytes is a signed assertion in queries[] that no check recomputes. HASH_ALLOWLIST = frozenset({"pin.py"}) #: Calls that start a process with an explicit environment, or change this process's. diff --git a/tests/support.py b/tests/support.py index f954870..e0ad17e 100644 --- a/tests/support.py +++ b/tests/support.py @@ -66,3 +66,82 @@ def write_stub(directory: Path, name: str, version: str, exit_code: int = 4, std ) path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) return path + + +class NetworkBlocked(RuntimeError): + """Raised by conftest.py's autouse guard when a test opens a connection or binds a port.""" + + +def synthetic_notebook( + *, + minor: int = 5, + indent: int | None = 1, + ensure_ascii: bool = False, + newline: str = "\n", + cells: list[dict[str, Any]] | None = None, +) -> str: + """A small notebook's text, written as nbformat writes it by default (sorted keys, indent 1, + a trailing newline, raw UTF-8), or in another style for the splice tests.""" + if cells is None: + cells = [ + { + "cell_type": "markdown", + "id": "intro", + "metadata": {}, + "source": ["# Example analysis\n", "\n", "Résumé of the method — café 実験.\n"], + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "load", + "metadata": {"tags": ["parameters"]}, + "outputs": [{"name": "stdout", "output_type": "stream", "text": ["1.50\n"]}], + "source": ["total = 1.50\n", "print(f'{total:.2f}')"], + }, + { + "cell_type": "code", + "execution_count": None, + "id": "plot", + "metadata": {}, + "outputs": [], + "source": ["values = [1e-3, 2.0, -0.0]"], + }, + ] + if minor < 5: + for cell in cells: + del cell["id"] + notebook = { + "cells": cells, + "metadata": { + "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, + "language_info": {"name": "python", "version": "3.12.0"}, + }, + "nbformat": 4, + "nbformat_minor": minor, + } + if indent is None: + return json.dumps(notebook, sort_keys=True, ensure_ascii=ensure_ascii) + text = json.dumps(notebook, sort_keys=True, indent=indent, ensure_ascii=ensure_ascii, separators=(",", ": ")) + return (text + "\n").replace("\n", newline) + + +def analysis_input(**extra: Any) -> dict[str, Any]: + """A scripted-recomputation envelope input for a file signed inline (``output_file``), with a + self-certifying signer whose identifier the CLI fills with the seed's did:key.""" + value: dict[str, Any] = { + "type": "content/analysis/v1", + "producerProfile": "scripted-recomputation/example", + "captureMethod": "script-run", + "prompt": "Run the example notebook end to end and record its output.", + "promptVisibility": "full_text", + "queries": [ + {"tool": "jupyter nbconvert --execute", "operationType": "script-run", "arguments": {"notebook": "x"}} + ], + "dataSources": [], + "cost": {"model": "none"}, + "skillMetadata": {}, + "trace": {"resourceSpans": []}, + "signer": {"bindingTier": "pseudonymous", "displayName": "Example analyst"}, + } + value.update(extra) + return value diff --git a/tests/test_badge.py b/tests/test_badge.py new file mode 100644 index 0000000..fb746fa --- /dev/null +++ b/tests/test_badge.py @@ -0,0 +1,301 @@ +"""Acceptance 2: ``badge_cell``. + +- The link and the Markdown equal what ``@typedstandards/host-core``'s own ``buildVerifyHref`` + and ``buildEmbedMarkdown`` print for the same URL (``fixtures/badge-golden.json``, captured + from typedstandards ``116882a`` ``packages/host-core/src/links.ts``), and the percent-encoding + equals the vendored Node's ``encodeURIComponent``, driven live. +- On a synthetic nbformat 4.5 notebook the badge is a markdown cell 0, and every other byte of + the file is unchanged. +- The cell carries no 64-hex string and no date or time. +- The Marimo form is a cell's source that renders the same Markdown with ``mo.md``. +""" + +from __future__ import annotations + +import ast +import hashlib +import json +import re +import subprocess +from pathlib import Path + +import nbformat +import pytest +from support import FIXTURES, synthetic_notebook + +import typedstandards +from typedstandards import badge_cell +from typedstandards._badge import badge_markdown, encode_uri_component, verify_href + +GOLDEN = FIXTURES / "badge-golden.json" +GOLDEN_SHA256 = "bd70f819249e8f8a5f3cf1245cbd521c32e3671623a4d3d6e009f71ae6661eaa" + +#: npstorey/typedstandards-host-template README.md line 9 at 70bfd18: the badge host-core printed +#: for the template's served bundle. +TEMPLATE_README_LINE_9 = ( + "[![Verify this record with Typed Standards](https://typedstandards.org/badge/typed-standards-verify.svg)]" + "()" +) + +BUNDLE_URL = "https://records.example.org/bundles/analysis.bundle.json" +#: A URL holding every character encodeURIComponent leaves unescaped besides letters and digits. +UNESCAPED_SET_URL = "https://records.example.org/a-b_c.d!e~f*g'h(i)j/k.bundle.json" + +HEX64 = re.compile(r"[0-9a-fA-F]{64}") +DATE_OR_TIME = re.compile(r"\d{4}-\d{2}-\d{2}|\d{2}:\d{2}") + + +def golden_cases() -> list[dict[str, str]]: + return json.loads(GOLDEN.read_text(encoding="utf-8"))["cases"] + + +def test_golden_fixture_is_the_captured_copy() -> None: + assert hashlib.sha256(GOLDEN.read_bytes()).hexdigest() == GOLDEN_SHA256 + + +# --- the link and the Markdown, against host-core ---------------------------------------------- + + +@pytest.mark.parametrize("case", golden_cases(), ids=lambda c: c["url"][:60]) +def test_link_and_markdown_equal_host_core(case: dict[str, str]) -> None: + assert verify_href(case["url"]) == case["verify"] + assert badge_markdown(case["url"]) == case["markdown"] + + +def test_template_readme_badge_is_reproduced() -> None: + url = "https://host-template.typedstandards.org/bundles/first-note.bundle.json" + assert badge_markdown(url) == TEMPLATE_README_LINE_9 + + +def test_unescaped_set_is_left_as_is() -> None: + href = verify_href(UNESCAPED_SET_URL) + assert href.endswith("records.example.org%2Fa-b_c.d!e~f*g'h(i)j%2Fk.bundle.json") + assert href == next(c["verify"] for c in golden_cases() if c["url"] == UNESCAPED_SET_URL) + + +ENCODING_PROBES = [ + UNESCAPED_SET_URL, + "https://records.example.org/path with space/x.json?q=1&r=a+b#frag", + "https://records.example.org/café/実験/\U0001f600/%41", + 'https://records.example.org/<>"[]{}|\\^`;/?:@&=+$,#', + "".join(chr(c) for c in range(0x20, 0x7F)), +] + + +def test_encoding_equals_node_encode_uri_component() -> None: + """Driven against a real encodeURIComponent: the Node that runs the CLI.""" + node = typedstandards.locate_node() + script = ( + "let s='';process.stdin.on('data',d=>s+=d)" + ".on('end',()=>process.stdout.write(JSON.stringify(JSON.parse(s).map(encodeURIComponent))))" + ) + proc = subprocess.run( + [node, "-e", script], input=json.dumps(ENCODING_PROBES).encode(), capture_output=True, check=True + ) + assert [encode_uri_component(p) for p in ENCODING_PROBES] == json.loads(proc.stdout) + + +def test_non_http_url_is_refused() -> None: + with pytest.raises(ValueError, match="http"): + verify_href("records.example.org/bundles/analysis.bundle.json") + + +# --- the Jupyter cell ------------------------------------------------------------------------- + + +def _write(tmp_path: Path, text: str, name: str = "analysis.ipynb") -> Path: + path = tmp_path / name + path.write_bytes(text.encode("utf-8")) + return path + + +def _read(path: Path) -> str: + return path.read_bytes().decode("utf-8") # read_text would translate CRLF + + +def _first_cell_offset(text: str) -> int: + return text.index("{", text.index('"cells"')) + + +def test_badge_is_cell_0_and_every_other_byte_is_kept(tmp_path: Path) -> None: + original = synthetic_notebook() + path = _write(tmp_path, original) + text = badge_cell(BUNDLE_URL, capture_method="script-run", notebook=path) + written = _read(path) + + # Every byte before and after the inserted cell is the author's. + at = _first_cell_offset(original) + assert written.startswith(original[:at]) + assert written.endswith(original[at:]) + inserted = written[at : len(written) - len(original) + at] + assert inserted.endswith(",\n ") + + notebook = json.loads(written) + before = json.loads(original) + assert notebook["cells"][1:] == before["cells"] + assert {k: v for k, v in notebook.items() if k != "cells"} == {k: v for k, v in before.items() if k != "cells"} + cell = notebook["cells"][0] + assert cell["cell_type"] == "markdown" + assert cell["id"] == "typedstandards-badge" + assert "".join(cell["source"]) == text + assert text.splitlines()[0] == badge_markdown(BUNDLE_URL) + assert text.splitlines()[0] == next(c["markdown"] for c in golden_cases() if c["url"] == BUNDLE_URL) + assert "| Host | `records.example.org` |" in text + assert "| Capture method | `script-run` |" in text + nbformat.validate(nbformat.reads(written, as_version=nbformat.NO_CONVERT)) + + +def test_inserted_cell_matches_nbformat_style(tmp_path: Path) -> None: + """The new cell is written as nbformat would write it: nbformat's own writer, given the + same notebook, gives the same bytes.""" + path = _write(tmp_path, synthetic_notebook()) + badge_cell(BUNDLE_URL, capture_method="script-run", notebook=path) + written = _read(path) + assert nbformat.writes(nbformat.reads(written, as_version=nbformat.NO_CONVERT)) + "\n" == written + + +def test_the_cell_names_no_hash_and_no_time(tmp_path: Path) -> None: + path = _write(tmp_path, synthetic_notebook()) + text = badge_cell(BUNDLE_URL, capture_method="script-run", notebook=path) + assert HEX64.search(text) is None + assert DATE_OR_TIME.search(text) is None + marimo = badge_cell(BUNDLE_URL, capture_method="script-run", marimo=True) + assert HEX64.search(marimo) is None + assert DATE_OR_TIME.search(marimo) is None + + +@pytest.mark.parametrize( + "url", + [ + "https://records.example.org/bundles/" + "ab" * 32 + ".bundle.json", + "https://records.example.org/2026-10-03/analysis.bundle.json", + ], + ids=["hash", "date"], +) +def test_a_url_carrying_a_hash_or_date_is_refused(tmp_path: Path, url: str) -> None: + original = synthetic_notebook() + path = _write(tmp_path, original) + with pytest.raises(ValueError, match="written before signing"): + badge_cell(url, capture_method="script-run", notebook=path) + assert _read(path) == original + + +def test_a_second_badge_is_refused(tmp_path: Path) -> None: + path = _write(tmp_path, synthetic_notebook()) + badge_cell(BUNDLE_URL, capture_method="script-run", notebook=path) + once = path.read_bytes() + with pytest.raises(ValueError, match="already has a cell with id 'typedstandards-badge'"): + badge_cell(BUNDLE_URL, capture_method="script-run", notebook=path) + assert path.read_bytes() == once + + +def test_same_call_same_bytes(tmp_path: Path) -> None: + first, second = _write(tmp_path, synthetic_notebook(), "a.ipynb"), _write(tmp_path, synthetic_notebook(), "b.ipynb") + badge_cell(BUNDLE_URL, capture_method="script-run", notebook=first) + badge_cell(BUNDLE_URL, capture_method="script-run", notebook=second) + assert first.read_bytes() == second.read_bytes() + + +@pytest.mark.parametrize("bad", ["", "has space", "x" * 65, "dot.ted"]) +def test_a_malformed_cell_id_is_refused(tmp_path: Path, bad: str) -> None: + path = _write(tmp_path, synthetic_notebook()) + with pytest.raises(ValueError, match="1 to 64 characters"): + badge_cell(BUNDLE_URL, capture_method="script-run", notebook=path, cell_id=bad) + + +@pytest.mark.parametrize("fact", ["host", "capture_method"]) +def test_a_fact_that_would_break_the_table_is_refused(fact: str) -> None: + kwargs = {"capture_method": "script-run", fact: "a | b"} + with pytest.raises(ValueError, match=fact): + badge_cell(BUNDLE_URL, **kwargs) + + +STYLES = { + "indent-2": {"indent": 2}, + "indent-0": {"indent": 0}, + "compact": {"indent": None}, + "crlf": {"newline": "\r\n"}, + "ensure-ascii": {"ensure_ascii": True}, + "nbformat-4.4": {"minor": 4}, + "no-cells": {"cells": []}, +} + + +@pytest.mark.parametrize("style", STYLES.values(), ids=STYLES.keys()) +def test_other_styles_keep_their_bytes_and_stay_valid(tmp_path: Path, style: dict) -> None: + original = synthetic_notebook(**style) + path = _write(tmp_path, original) + badge_cell(BUNDLE_URL, capture_method="script-run", notebook=path) + written = _read(path) + notebook = json.loads(written) + assert notebook["cells"][1:] == json.loads(original)["cells"] + if style.get("minor", 5) < 5: + assert "id" not in notebook["cells"][0] + else: + assert notebook["cells"][0]["id"] == "typedstandards-badge" + if json.loads(original)["cells"]: + at = _first_cell_offset(original) + assert written.startswith(original[:at]) and written.endswith(original[at:]) + nbformat.validate(nbformat.reads(written, as_version=nbformat.NO_CONVERT)) + # The file's own style is the inserted cell's: re-serialising the result in that style + # reproduces it byte for byte. + newline = style.get("newline", "\n") + indent = style.get("indent", 1) + if indent is None: + again = json.dumps(notebook, sort_keys=True, ensure_ascii=style.get("ensure_ascii", False)) + else: + again = json.dumps( + notebook, + sort_keys=True, + indent=indent, + ensure_ascii=style.get("ensure_ascii", False), + separators=(",", ": "), + ) + again = (again + "\n").replace("\n", newline) + assert again == written + + +def test_unusual_bytes_elsewhere_survive(tmp_path: Path) -> None: + """Unsorted keys, a number spelled 1.50E0 and an escaped slash are not re-serialised.""" + original = synthetic_notebook().replace( + ' "language": "python",\n', ' "language": "python",\n "z": 1.50E0,\n "s": "a\\/b",\n', 1 + ) + assert '"z": 1.50E0' in original + path = _write(tmp_path, original) + badge_cell(BUNDLE_URL, capture_method="script-run", notebook=path) + written = _read(path) + at = _first_cell_offset(original) + assert written.startswith(original[:at]) and written.endswith(original[at:]) + assert '"z": 1.50E0,\n "s": "a\\/b",' in written + + +# --- the Marimo form ---------------------------------------------------------------------------- + + +def _md_argument(source: str) -> str: + call = ast.parse(source).body[0].value # type: ignore[attr-defined] + assert isinstance(call, ast.Call) and ast.unparse(call.func) == "mo.md" + return ast.literal_eval(call.args[0]) + + +def test_marimo_form_is_an_md_cell_with_the_same_badge() -> None: + source = badge_cell(BUNDLE_URL, capture_method="script-run", marimo=True) + assert source.startswith("mo.md(") + assert _md_argument(source) == badge_cell(BUNDLE_URL, capture_method="script-run") + assert _md_argument(source).splitlines()[0] == badge_markdown(BUNDLE_URL) + + +def test_marimo_renders_the_link() -> None: + """Driven against marimo itself: its Markdown renders the angle-bracket destination as the link.""" + import marimo as mo + + source = badge_cell(BUNDLE_URL, capture_method="script-run", marimo=True) + html = eval(compile(source, "", "eval"), {"mo": mo}).text # noqa: S307 - our own generated source + assert f'href="{verify_href(BUNDLE_URL)}"' in html + assert 'src="https://typedstandards.org/badge/typed-standards-verify.svg"' in html + + +def test_marimo_form_refuses_a_notebook(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="marimo"): + badge_cell(BUNDLE_URL, capture_method="script-run", marimo=True, notebook=tmp_path / "x.ipynb") diff --git a/tests/test_comparison.py b/tests/test_comparison.py new file mode 100644 index 0000000..746f414 --- /dev/null +++ b/tests/test_comparison.py @@ -0,0 +1,225 @@ +"""Acceptance 3: ``comparison_cell``. + +It appends the spec §8.7.4 comparison cell as the notebook's last code cell, with the caller's +values as literals, the recompute line and the delta lines; the notebook stays valid JSON that +the real CLI signs inline; a value that is not a literal is refused. +""" + +from __future__ import annotations + +import ast +import contextlib +import enum +import io +import json +from datetime import UTC, datetime, timedelta, timezone +from pathlib import Path +from typing import Any + +import nbformat +import pytest +from support import analysis_input, synthetic_notebook + +import typedstandards +from typedstandards import comparison_cell + +VALUES = { + "rows": 1204, + "mean_fare": 13.75, + "share_late": 0.0825, + "district": "Café “north”", + "complete": True, + "note": None, + "by_month": [101, 99.5, None], + "split": {"weekday": 812, "weekend": 392}, +} +RECOMPUTE = "recompute_key_metrics()" +CAPTURED_AT = "2026-10-03T12:00:00Z" + +EXPECTED_SOURCE = ( + "# ORIGINAL VALUES (captured at executedAt = 2026-10-03T12:00:00Z)\n" + "original = {\n" + ' "rows": 1204,\n' + ' "mean_fare": 13.75,\n' + ' "share_late": 0.0825,\n' + ' "district": "Café “north”",\n' + ' "complete": True,\n' + ' "note": None,\n' + ' "by_month": [101, 99.5, None],\n' + ' "split": {"weekday": 812, "weekend": 392},\n' + "}\n" + "\n" + "# CURRENT VALUES (re-computed against live data using the same helpers + queries above)\n" + "current = recompute_key_metrics()\n" + "\n" + "# DELTAS\n" + "for k in original:\n" + " delta = (current[k] - original[k]) if isinstance(original[k], (int, float)) else (original[k], current[k])\n" + ' print(f"{k}: original={original[k]}, current={current[k]}, delta={delta}")\n' +) + + +def _notebook(tmp_path: Path, **style: Any) -> tuple[Path, str]: + text = synthetic_notebook(**style) + path = tmp_path / "analysis.ipynb" + path.write_bytes(text.encode("utf-8")) + return path, text + + +def _read(path: Path) -> str: + return path.read_bytes().decode("utf-8") + + +def test_appends_the_spec_shaped_cell_last(tmp_path: Path) -> None: + path, original = _notebook(tmp_path) + source = comparison_cell(path, VALUES, recompute=RECOMPUTE, captured_at=CAPTURED_AT) + assert source == EXPECTED_SOURCE + + written = _read(path) + notebook = json.loads(written) # still valid JSON + before = json.loads(original) + assert notebook["cells"][:-1] == before["cells"] + cell = notebook["cells"][-1] + assert cell == { + "cell_type": "code", + "execution_count": None, + "id": "typedstandards-comparison", + "metadata": {}, + "outputs": [], + "source": source.splitlines(keepends=True), + } + # Every byte before the new cell is the author's, and so is the file's tail. + last_end = original.rindex("}", 0, original.index('"metadata": {\n "kernelspec"')) + 1 + assert written.startswith(original[:last_end]) + assert written.endswith(original[last_end:]) + nbformat.validate(nbformat.reads(written, as_version=nbformat.NO_CONVERT)) + + +def test_the_values_are_literals_that_read_back(tmp_path: Path) -> None: + path, _ = _notebook(tmp_path) + source = comparison_cell(path, VALUES, recompute=RECOMPUTE, captured_at=CAPTURED_AT) + tree = ast.parse(source) + original = next(n for n in tree.body if isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == "original") + assert ast.literal_eval(original.value) == VALUES + current = next(n for n in tree.body if isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == "current") + assert ast.unparse(current.value) == RECOMPUTE + + +def test_the_cell_runs_and_prints_the_deltas(tmp_path: Path) -> None: + path, _ = _notebook(tmp_path) + values = {"rows": 1204, "mean_fare": 13.75, "district": "north"} + source = comparison_cell(path, values, recompute="recompute()", captured_at=CAPTURED_AT) + out = io.StringIO() + with contextlib.redirect_stdout(out): + exec(source, {"recompute": lambda: {"rows": 1210, "mean_fare": 14.0, "district": "south"}}) # noqa: S102 + assert out.getvalue().splitlines() == [ + "rows: original=1204, current=1210, delta=6", + "mean_fare: original=13.75, current=14.0, delta=0.25", + "district: original=north, current=south, delta=('north', 'south')", + ] + + +def test_the_real_cli_signs_the_notebook_inline(tmp_path: Path, seed: str) -> None: + """With a throwaway seed (the ``seed`` fixture), through the wrapper, exit 0.""" + path, _ = _notebook(tmp_path) + typedstandards.badge_cell( + "https://records.example.org/bundles/analysis.bundle.json", capture_method="script-run", notebook=path + ) + comparison_cell(path, VALUES, recompute=RECOMPUTE, captured_at=CAPTURED_AT) + signed = typedstandards.sign(analysis_input(), output_file=path) # raises on any non-zero exit + assert signed["package"]["output"] == _read(path) + assert signed["package"]["contentCanonicalization"].endswith("/raw-bytes/v1") + assert typedstandards.verify(signed)["ok"] is True + + +def test_captured_at_from_a_datetime(tmp_path: Path) -> None: + path, _ = _notebook(tmp_path) + when = datetime(2026, 10, 3, 8, 0, tzinfo=timezone(timedelta(hours=-4))) + source = comparison_cell(path, {"rows": 1}, recompute=RECOMPUTE, captured_at=when) + assert source.startswith("# ORIGINAL VALUES (captured at executedAt = 2026-10-03T12:00:00.000Z)\n") + + +class Status(enum.IntEnum): + OK = 3 + + +class Fare(float): + def __repr__(self) -> str: + return "Fare(...)" + + +def test_int_and_float_subclasses_are_written_as_their_value(tmp_path: Path) -> None: + path, _ = _notebook(tmp_path) + source = comparison_cell( + path, {"status": Status.OK, "fare": Fare(2.5)}, recompute=RECOMPUTE, captured_at=CAPTURED_AT + ) + assert ' "status": 3,\n "fare": 2.5,\n' in source + + +class Thing: + pass + + +NOT_LITERALS = { + "object": Thing(), + "function": lambda: 1, + "builtin": len, + "tuple": (1, 2), + "set": {1, 2}, + "bytes": b"x", + "complex": 1j, + "datetime": datetime(2026, 10, 3, tzinfo=UTC), + "nested object": {"a": [1, Thing()]}, + "non-str key": {1: "a"}, +} + + +@pytest.mark.parametrize("value", NOT_LITERALS.values(), ids=NOT_LITERALS.keys()) +def test_a_value_that_is_not_a_literal_is_refused(tmp_path: Path, value: Any) -> None: + path, original = _notebook(tmp_path) + with pytest.raises(TypeError, match="not a literal|not a string"): + comparison_cell(path, {"rows": 1, "bad": value}, recompute=RECOMPUTE, captured_at=CAPTURED_AT) + assert _read(path) == original + + +@pytest.mark.parametrize("value", [float("nan"), float("inf"), -float("inf")]) +def test_a_non_finite_float_is_refused(tmp_path: Path, value: float) -> None: + path, original = _notebook(tmp_path) + with pytest.raises(ValueError, match="no Python literal"): + comparison_cell(path, {"bad": value}, recompute=RECOMPUTE, captured_at=CAPTURED_AT) + assert _read(path) == original + + +@pytest.mark.parametrize("recompute", ["", "x = recompute()", "a()\nb()", "import os", "recompute("]) +def test_a_recompute_that_is_not_one_expression_is_refused(tmp_path: Path, recompute: str) -> None: + path, _ = _notebook(tmp_path) + with pytest.raises(ValueError, match="recompute"): + comparison_cell(path, {"rows": 1}, recompute=recompute, captured_at=CAPTURED_AT) + + +@pytest.mark.parametrize("when", ["yesterday", "2026-10-03T12:00:00Z\nprint(1)", datetime(2026, 10, 3)]) +def test_a_bad_capture_time_is_refused(tmp_path: Path, when: Any) -> None: + path, _ = _notebook(tmp_path) + with pytest.raises(ValueError, match="captured_at"): + comparison_cell(path, {"rows": 1}, recompute=RECOMPUTE, captured_at=when) + + +def test_no_values_is_refused(tmp_path: Path) -> None: + path, _ = _notebook(tmp_path) + with pytest.raises(ValueError, match="non-empty"): + comparison_cell(path, {}, recompute=RECOMPUTE, captured_at=CAPTURED_AT) + + +def test_a_second_comparison_cell_is_refused(tmp_path: Path) -> None: + path, _ = _notebook(tmp_path) + comparison_cell(path, {"rows": 1}, recompute=RECOMPUTE, captured_at=CAPTURED_AT) + with pytest.raises(ValueError, match="already has a cell with id 'typedstandards-comparison'"): + comparison_cell(path, {"rows": 1}, recompute=RECOMPUTE, captured_at=CAPTURED_AT) + + +def test_ascii_escaped_notebook_stays_ascii(tmp_path: Path) -> None: + path, _ = _notebook(tmp_path, ensure_ascii=True) + comparison_cell(path, VALUES, recompute=RECOMPUTE, captured_at=CAPTURED_AT) + written = path.read_bytes() + assert written.isascii() + assert "".join(json.loads(written)["cells"][-1]["source"]) == EXPECTED_SOURCE diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py new file mode 100644 index 0000000..c20c009 --- /dev/null +++ b/tests/test_fixtures.py @@ -0,0 +1,36 @@ +"""The captured record fixtures (``fixtures/capture_records.py``): each is pinned by SHA-256, and +each bundle's captured ``verify --json`` document is what the vendored CLI prints for it today.""" + +from __future__ import annotations + +import hashlib +import json + +import pytest +from support import FIXTURES + +import typedstandards + +PINNED = { + "record-active.signed.json": "6a43fcbbd205a94ad71b94c7600b61c740177568b345af2744a947236285abfc", + "record-active.bundle.json": "aa75bd9ad6f7c101ee2c3a96373e97826e6fae6dfa172bdc1e84bf69efcf5937", + "record-active.verify.json": "a37890a4413b0ff206b0fc19f0cc998732c6b9590d504d5639b7ebbf3fccc833", + "record-withdrawn.signed.json": "cbc91d561707f1e10034ff01a5cd587fb52276bae79f626665b14aa8d4bf5a9b", + "record-withdrawn.withdrawal.json": "42bfe9b0ee86ba5b219c62a472a1d0e8ee45b5191fc7a7dd8cde842b69859a2e", + "record-withdrawn.bundle.json": "33b608d24e36beca3314fde938513252380e2ffbeaff3312e4dcb4f58482d2eb", + "record-withdrawn.verify.json": "8b882a0bc06300add88b5186b11861279a1f6505f80344d1c279aa1eaa3ad4ef", +} + + +@pytest.mark.parametrize("name", PINNED) +def test_fixture_is_the_captured_copy(name: str) -> None: + assert hashlib.sha256((FIXTURES / name).read_bytes()).hexdigest() == PINNED[name] + + +@pytest.mark.parametrize("record", ["active", "withdrawn"]) +def test_captured_verify_is_what_the_cli_prints(record: str) -> None: + bundle = json.loads((FIXTURES / f"record-{record}.bundle.json").read_text(encoding="utf-8")) + captured = json.loads((FIXTURES / f"record-{record}.verify.json").read_text(encoding="utf-8")) + assert typedstandards.verify(bundle) == captured + assert captured["ok"] is True + assert captured["lifecycle"]["status"] == record diff --git a/tests/test_guards.py b/tests/test_guards.py index 0b77ed2..034cfd1 100644 --- a/tests/test_guards.py +++ b/tests/test_guards.py @@ -38,6 +38,11 @@ def test_package_imports_no_digest_module() -> None: assert hash_imports(PACKAGE) == [] +def test_only_pin_imports_a_digest_module() -> None: + """pin.py is allowlisted for its one digest, a signed assertion; no other module joins it.""" + assert {line.split(":", 1)[0] for line in hash_imports(PACKAGE, allow=frozenset())} == {"pin.py"} + + def test_package_runs_no_global_cli() -> None: """The wrapper runs the vendored entry file, never npx or a typedstandards on PATH.""" for path in PACKAGE.rglob("*.py"): diff --git a/tests/test_offline.py b/tests/test_offline.py new file mode 100644 index 0000000..3559578 --- /dev/null +++ b/tests/test_offline.py @@ -0,0 +1,45 @@ +"""Acceptance 6: no test opens a network connection or binds a port. + +``conftest.py``'s autouse ``no_network`` fixture replaces ``socket.socket.connect``, +``connect_ex``, ``bind`` and ``socket.create_connection`` with functions that raise +:class:`support.NetworkBlocked`, for every test. These tests drive a real attempt of each kind, +so a guard that stops guarding fails here. +""" + +from __future__ import annotations + +import socket + +import httpx +import pytest +from support import NetworkBlocked + + +def test_create_connection_is_blocked() -> None: + with pytest.raises(NetworkBlocked, match="network"): + socket.create_connection(("127.0.0.1", 9), timeout=1) + + +def test_connect_is_blocked() -> None: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(1) + with pytest.raises(NetworkBlocked, match="network"): + sock.connect(("127.0.0.1", 9)) + + +def test_connect_ex_is_blocked() -> None: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + with pytest.raises(NetworkBlocked, match="network"): + sock.connect_ex(("127.0.0.1", 9)) + + +def test_bind_is_blocked() -> None: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + with pytest.raises(NetworkBlocked, match="port"): + sock.bind(("127.0.0.1", 0)) + + +def test_an_http_client_without_a_mock_transport_is_blocked() -> None: + """httpx's real transport reaches the guard, so a helper test that forgot its mock fails.""" + with httpx.Client() as client, pytest.raises(NetworkBlocked): + client.get("http://127.0.0.1:9/") diff --git a/tests/test_pin.py b/tests/test_pin.py new file mode 100644 index 0000000..fee0074 --- /dev/null +++ b/tests/test_pin.py @@ -0,0 +1,200 @@ +"""Acceptance 1: ``pin``. + +Over ``httpx.MockTransport`` (the autouse guard fails any real connection), ``pin(url)`` returns +the bytes and a retrieval entry carrying ``url``, ``sha256``, ``bytes``, ``httpStatus`` and +``fetchedAt``; for a portal-shaped URL, a second request reads ``rowsUpdatedAt`` from +``/api/views/``. The digest follows the body. The host is fictional (``data.example.org``). +""" + +from __future__ import annotations + +import hashlib +import json +from datetime import UTC, datetime +from pathlib import Path + +import httpx +import pytest +from support import analysis_input + +import typedstandards +from typedstandards import Pinned, pin +from typedstandards.pin import portal_dataset_id + +BODY = b"id,district,fare\n1,north,13.75\n2,south,9.50\n" +FIXED = datetime(2026, 10, 3, 12, 0, 0, 250000, tzinfo=UTC) +PORTAL_CSV = "https://data.example.org/resource/abcd-1234.csv" +ROWS_UPDATED_AT = 1790991539 + + +def clock() -> datetime: + return FIXED + + +def portal(body: bytes = BODY, *, rows_updated_at: object = ROWS_UPDATED_AT, seen: list[str] | None = None): + def handler(request: httpx.Request) -> httpx.Response: + if seen is not None: + seen.append(str(request.url)) + if request.url.path == "/api/views/abcd-1234": + return httpx.Response( + 200, + json={ + "id": "abcd-1234", + "rowsUpdatedAt": rows_updated_at, + "viewLastModified": 1790991000, + "publicationDate": 1700000000, + }, + ) + if request.url.path in {"/resource/abcd-1234.csv", "/api/views/abcd-1234/rows.csv", "/files/data.csv"}: + return httpx.Response(200, content=body, headers={"content-type": "text/csv"}) + return httpx.Response(404) + + return httpx.MockTransport(handler) + + +def test_returns_the_bytes_and_the_entry() -> None: + url = "https://files.example.org/files/data.csv" + result = pin(url, transport=portal(), clock=clock) + assert isinstance(result, Pinned) + content, entry = result + assert content == BODY + assert entry == { + "tool": "typedstandards.pin", + "operationType": "retrieve", + "arguments": { + "url": url, + "sha256": hashlib.sha256(BODY).hexdigest(), + "bytes": len(BODY), + "httpStatus": 200, + "fetchedAt": "2026-10-03T12:00:00.250Z", + }, + } + + +def test_a_changed_body_changes_the_digest() -> None: + url = "https://files.example.org/files/data.csv" + first = pin(url, transport=portal(BODY), clock=clock).entry["arguments"] + second = pin(url, transport=portal(BODY + b"3,east,4.00\n"), clock=clock).entry["arguments"] + assert first["sha256"] != second["sha256"] + assert second["sha256"] == hashlib.sha256(BODY + b"3,east,4.00\n").hexdigest() + assert second["bytes"] == len(BODY) + len(b"3,east,4.00\n") + + +@pytest.mark.parametrize("body", [b"", b"x", BODY, bytes(range(256)) * 4], ids=["empty", "one", "csv", "binary"]) +def test_the_digest_follows_the_body(body: bytes) -> None: + content, entry = pin("https://files.example.org/files/data.csv", transport=portal(body), clock=clock) + assert content == body + assert entry["arguments"]["sha256"] == hashlib.sha256(body).hexdigest() + assert entry["arguments"]["bytes"] == len(body) + + +@pytest.mark.parametrize( + "url", [PORTAL_CSV, "https://data.example.org/api/views/abcd-1234/rows.csv"], ids=["resource", "rows"] +) +def test_a_portal_resource_carries_rows_updated_at(url: str) -> None: + seen: list[str] = [] + content, entry = pin(url, transport=portal(seen=seen), clock=clock) + assert content == BODY + assert seen == [url, "https://data.example.org/api/views/abcd-1234"] + assert entry["arguments"]["rowsUpdatedAt"] == ROWS_UPDATED_AT + assert entry["datasetId"] == "abcd-1234" + assert entry["arguments"]["sha256"] == hashlib.sha256(BODY).hexdigest() + + +def test_a_moved_portal_reads_as_a_different_entry() -> None: + before = pin(PORTAL_CSV, transport=portal(BODY, rows_updated_at=ROWS_UPDATED_AT), clock=clock).entry + after = pin( + PORTAL_CSV, transport=portal(BODY + b"3,east,4.00\n", rows_updated_at=ROWS_UPDATED_AT + 86400), clock=clock + ).entry + assert before["arguments"]["sha256"] != after["arguments"]["sha256"] + assert before["arguments"]["rowsUpdatedAt"] != after["arguments"]["rowsUpdatedAt"] + + +@pytest.mark.parametrize( + ("url", "expected"), + [ + ("https://data.example.org/resource/abcd-1234.csv", "abcd-1234"), + ("https://data.example.org/resource/abcd-1234.geojson", "abcd-1234"), + ("https://data.example.org/resource/abcd-1234", "abcd-1234"), + ("https://data.example.org/resource/abcd-1234.json?$limit=5", "abcd-1234"), + ("https://other.example.net/api/views/a1b2-c3d4/rows.csv?accessType=DOWNLOAD", "a1b2-c3d4"), + ("https://data.example.org/resource/ABCD-1234.csv", None), + ("https://data.example.org/resource/abcd-12345.csv", None), + ("https://data.example.org/api/views/abcd-1234", None), + ("https://data.example.org/files/abcd-1234.csv", None), + ], +) +def test_portal_shape_is_recognised_by_path_not_host(url: str, expected: str | None) -> None: + assert portal_dataset_id(url) == expected + + +def test_portal_metadata_can_be_skipped_or_required() -> None: + seen: list[str] = [] + entry = pin(PORTAL_CSV, transport=portal(seen=seen), clock=clock, portal_metadata=False).entry + assert seen == [PORTAL_CSV] + assert "rowsUpdatedAt" not in entry["arguments"] and "datasetId" not in entry + with pytest.raises(ValueError, match="portal resource's shape"): + pin("https://files.example.org/files/data.csv", transport=portal(), clock=clock, portal_metadata=True) + + +@pytest.mark.parametrize("value", [None, "1790991539", 1790991539.5, True]) +def test_a_portal_without_epoch_rows_updated_at_is_refused(value: object) -> None: + with pytest.raises(ValueError, match="rowsUpdatedAt"): + pin(PORTAL_CSV, transport=portal(rows_updated_at=value), clock=clock) + + +def test_licence_and_dataset_id_are_the_callers() -> None: + entry = pin( + "https://files.example.org/files/data.csv", + transport=portal(), + clock=clock, + licence="CC-BY-4.0", + dataset_id="fares", + ).entry + assert entry["arguments"]["licence"] == "CC-BY-4.0" + assert entry["datasetId"] == "fares" + + +def test_a_failed_fetch_raises() -> None: + with pytest.raises(httpx.HTTPStatusError): + pin("https://files.example.org/missing.csv", transport=portal(), clock=clock) + + +def test_a_redirect_is_followed_and_recorded() -> None: + def handler(request: httpx.Request) -> httpx.Response: + if request.url.path == "/old.csv": + return httpx.Response(301, headers={"location": "https://files.example.org/files/data.csv"}) + return httpx.Response(200, content=BODY) + + entry = pin("https://files.example.org/old.csv", transport=httpx.MockTransport(handler), clock=clock).entry + assert entry["arguments"]["url"] == "https://files.example.org/old.csv" + assert entry["arguments"]["finalUrl"] == "https://files.example.org/files/data.csv" + assert entry["arguments"]["httpStatus"] == 200 + + +def test_an_injected_client_is_used_and_left_open() -> None: + with httpx.Client(transport=portal()) as client: + content, _ = pin("https://files.example.org/files/data.csv", client=client, clock=clock) + assert content == BODY + assert not client.is_closed + + +def test_a_naive_clock_is_refused() -> None: + with pytest.raises(ValueError, match="timezone-aware"): + pin("https://files.example.org/files/data.csv", transport=portal(), clock=lambda: datetime(2026, 10, 3)) + + +def test_save_writes_the_bytes_and_the_entry(tmp_path: Path) -> None: + target = tmp_path / "data.csv" + _, entry = pin(PORTAL_CSV, transport=portal(), clock=clock, save=target) + assert target.read_bytes() == BODY + written = (tmp_path / "data.csv.pin.json").read_text(encoding="utf-8") + assert json.loads(written) == entry + assert written.endswith("}\n") + + +def test_the_entry_is_signable_in_queries(seed: str) -> None: + """The real CLI accepts the entry in a record's queries[] and signs it as given.""" + _, entry = pin(PORTAL_CSV, transport=portal(), clock=clock, licence="CC-BY-4.0") + signed = typedstandards.sign(analysis_input(queries=[entry], output="a test output")) + assert signed["package"]["queries"] == [entry] diff --git a/tests/test_show.py b/tests/test_show.py new file mode 100644 index 0000000..045b87f --- /dev/null +++ b/tests/test_show.py @@ -0,0 +1,270 @@ +"""Acceptance 5: ``show``. + +From the captured ``verify --json`` documents (``fixtures/record-*.verify.json``, with their +bundles) it renders the type and role, the abbreviated signer with "Signed with a +self-certifying key", the display name marked as the signer's own description, the first 12 hex +of the hash, ``createdAt``, ``vcsRef`` marked "asserted; not fetched", the status with its +reason or successor, one line per check, and the spec §9.3 sentence. Rendering needs no Node. +The Jupyter form has ``_repr_html_``; the Marimo form is ``mo.Html`` of the same HTML. The +output is byte-stable (``fixtures/show-*.html``). Every string from the record is escaped. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import re +import subprocess +import sys +import types +from typing import Any + +import pytest +from support import FIXTURES + +from typedstandards import Shown, show +from typedstandards._show import KEY_DERIVED_MATCH_LABEL, NOT_CORRECTNESS, SELF_CERTIFIED_LABEL + + +def load(name: str) -> dict[str, Any]: + return json.loads((FIXTURES / name).read_text(encoding="utf-8")) + + +def pair(record: str) -> tuple[dict[str, Any], dict[str, Any]]: + return load(f"record-{record}.bundle.json"), load(f"record-{record}.verify.json") + + +def html_of(record: str) -> str: + bundle, result = pair(record) + return show(bundle, result).html + + +# --- what it renders ------------------------------------------------------------------------ + + +@pytest.mark.parametrize("record", ["active", "withdrawn"]) +def test_renders_the_record_and_its_checks(record: str) -> None: + bundle, result = pair(record) + package = bundle["package"] + html = show(bundle, result).html + identifier = package["signer"]["identifier"] + + assert "content/analysis/v1" in html + role = package["extensions"]["role"] + assert f'{role} (asserted by the signer in extensions["role"]' in html + abbreviated = f"did:key:{identifier[8:16]}…{identifier[-6:]}" + assert f'{abbreviated} · {SELF_CERTIFIED_LABEL}' in html + assert "calls itself “Example analyst”" in html + assert "(the signer’s own description; not verified)" in html + assert f">{result['nodeId'][:12]}…" in html + assert f"{package['metadata']['createdAt']}" in html + vcs = package["vcsRef"] + assert ( + f"{vcs['repoUrl']}, commit {vcs['commitSha']}, path {vcs['path']}" + in html + ) + assert "(asserted; not fetched)" in html + assert NOT_CORRECTNESS in html + assert html.count(NOT_CORRECTNESS) == 1 + + # One line per check verify-core reported, each numbered as spec §9.2 numbers it. + lines = re.findall(r"
  • (#\d+) [^:<]+:", html) + assert lines == ["#1", "#2", "#3", "#4", "#5", "#6", "#7", "#8", "#9", "#10", "#11", "#12", "#14", "#15", "#16"] + assert f"#14 Signer identity: {KEY_DERIVED_MATCH_LABEL}" in html + assert f"#5 Key trust: {SELF_CERTIFIED_LABEL}" in html + + +def test_active_status() -> None: + row = re.search(r">Status]*>(.*?)", html_of("active")) + assert row is not None and row.group(1) == "active" + + +def test_a_withdrawn_record_renders_its_reason() -> None: + bundle, result = pair("withdrawn") + html = show(bundle, result).html + reason = result["lifecycle"]["withdrawnReason"] + assert reason == "The claim compared periods of different lengths; a restatement replaces it." + assert f"withdrawn, reason: {reason}" in html + assert "#10 Lifecycle: withdrawn" in html + + +def test_a_superseded_record_renders_its_successor() -> None: + bundle, result = pair("active") + successor = "f" * 64 + lifecycle = {"status": "superseded", "source": "attestation-chain", "chain": [], "successorNodeId": successor} + result = {**result, "lifecycle": {**lifecycle, "supersededAt": "2026-10-04T09:00:00.000Z"}} + html = show(bundle, result).html + assert f'superseded, successor: ffffffffffff…' in html + + +def test_no_check_mark_and_no_person_claims() -> None: + for record in ("active", "withdrawn"): + html = html_of(record) + for mark in ("✓", "✔", "☑", "✅", "✓", "✓"): + assert mark not in html + assert "same person" not in html and "same publisher" not in html + assert "#14 Signer identity: ok" not in html + + +def test_a_registry_bound_signer_is_not_labelled_self_certifying() -> None: + bundle, result = pair("active") + result = copy.deepcopy(result) + result["checks"]["keyTrust"] = {"status": "active", "verified": True, "kid": "example:key-1"} + result["checks"]["signerIdentity"] = {"status": "ok"} + html = show(bundle, result).html + assert SELF_CERTIFIED_LABEL not in html + assert "key status active" in html + assert "#14 Signer identity: ok" in html + + +def test_a_failed_verdict_lists_its_failures() -> None: + bundle, result = pair("active") + result = {**result, "ok": False, "failures": ["content_hash_mismatch"]} + html = show(bundle, result).html + assert "verify: failed, content_hash_mismatch" in html + + +def test_role_path_reads_another_layout() -> None: + bundle, result = pair("active") + bundle = copy.deepcopy(bundle) + bundle["package"]["extensions"] = {"org.example.notebook": {"role": ["analysis", "source"]}} + html = show(bundle, result, role_path=("org.example.notebook", "role")).html + assert "analysis, source None: + """Two renders are equal, and equal to the committed rendering of the same fixture.""" + first, second = html_of(record), html_of(record) + assert first == second + golden = (FIXTURES / f"show-{record}.html").read_bytes().decode("utf-8") + assert first + "\n" == golden + + +def test_output_is_the_same_in_another_process() -> None: + code = ( + "import json, sys; sys.path.insert(0, 'tests'); from support import FIXTURES; import typedstandards as t; " + "b = json.loads((FIXTURES / 'record-withdrawn.bundle.json').read_text('utf-8')); " + "r = json.loads((FIXTURES / 'record-withdrawn.verify.json').read_text('utf-8')); " + "sys.stdout.buffer.write(t.show(b, r).html.encode('utf-8'))" + ) + out = subprocess.run([sys.executable, "-c", code], capture_output=True, check=True).stdout.decode("utf-8") + assert hashlib.sha256(out.encode()).hexdigest() == hashlib.sha256(html_of("withdrawn").encode()).hexdigest() + + +# --- escaping ----------------------------------------------------------------------------------- + +SCRIPT = "" + + +def _inject(where: str) -> tuple[dict[str, Any], dict[str, Any]]: + bundle, result = copy.deepcopy(pair("withdrawn")) + package = bundle["package"] + if where == "displayName": + package["signer"]["displayName"] = SCRIPT + elif where == "bindingTier": + package["signer"]["bindingTier"] = SCRIPT + elif where == "identifier": + package["signer"]["identifier"] = "did:key:" + SCRIPT * 3 + elif where == "role": + package["extensions"]["role"] = SCRIPT + elif where == "summary": + package["summary"] = SCRIPT + elif where == "type": + package["type"] = SCRIPT + elif where == "vcsRef": + package["vcsRef"] = {"repoUrl": SCRIPT, "commitSha": SCRIPT, "path": SCRIPT, "ref": SCRIPT} + elif where == "createdAt": + package["metadata"]["createdAt"] = SCRIPT + elif where == "subjectTitle": + bundle["subjectTitle"] = SCRIPT + elif where == "withdrawnReason": + result["lifecycle"]["withdrawnReason"] = SCRIPT + elif where == "failures": + result["ok"] = False + result["failures"] = [SCRIPT] + elif where == "check status": + result["checks"]["contentProfile"]["status"] = SCRIPT + elif where == "unknown check": + result["checks"]["futureCheck"] = {"status": SCRIPT} + return bundle, result + + +INJECTION_POINTS = [ + "displayName", "bindingTier", "identifier", "role", "summary", "type", "vcsRef", "createdAt", + "subjectTitle", "withdrawnReason", "failures", "check status", "unknown check", +] # fmt: skip + + +@pytest.mark.parametrize("where", INJECTION_POINTS) +def test_every_string_from_the_record_is_escaped(where: str) -> None: + html = show(*_inject(where)).html + assert " None: + shown = show(*pair("active")) + assert isinstance(shown, Shown) + assert shown._repr_html_() == shown.html + + +def test_marimo_form_with_a_stand_in(monkeypatch: pytest.MonkeyPatch) -> None: + class Html: + def __init__(self, text: str) -> None: + self.text = text + + stand_in = types.ModuleType("marimo") + stand_in.Html = Html # type: ignore[attr-defined] + monkeypatch.setitem(sys.modules, "marimo", stand_in) + shown = show(*pair("active"), marimo=True) + assert isinstance(shown, Html) + assert shown.text == html_of("active") + + +def test_marimo_form_with_marimo() -> None: + import marimo as mo + + shown = show(*pair("withdrawn"), marimo=True) + assert isinstance(shown, mo.Html) + assert shown.text == html_of("withdrawn") + + +def test_without_a_result_show_asks_the_cli() -> None: + bundle, result = pair("withdrawn") + assert show(bundle).html == show(bundle, result).html + + +def test_without_a_result_a_record_that_fails_renders_its_failure() -> None: + bundle, _ = pair("active") + bundle = copy.deepcopy(bundle) + bundle["package"]["output"] += " " + html = show(bundle).html + assert "verify: failed," in html + + +def test_paths_are_accepted() -> None: + html = show(FIXTURES / "record-active.bundle.json", FIXTURES / "record-active.verify.json").html + assert html == html_of("active") + + +def test_what_has_no_package_is_refused() -> None: + with pytest.raises(ValueError, match="package inline"): + show({"packageHash": "0" * 64}, {"ok": True}) + + +def test_importing_the_package_loads_no_notebook_library() -> None: + code = ( + "import sys, typedstandards; " + "print(sorted(m for m in ('httpx', 'yaml', 'marimo', 'IPython', 'nbformat') if m in sys.modules))" + ) + out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, check=True).stdout + assert out.strip() == "[]" diff --git a/tests/test_sidecar.py b/tests/test_sidecar.py new file mode 100644 index 0000000..e21bfd0 --- /dev/null +++ b/tests/test_sidecar.py @@ -0,0 +1,117 @@ +"""Acceptance 4: ``sidecar``. + +It writes ``.record.yaml`` from a real ``view`` of a signed fixture (the CLI +builds the view in the test) with the §8.8.1 field set, without ``package`` or ``trustRegistry``; +``yaml.safe_load`` of the file equals the JSON view on every field both express. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest +import yaml +from support import BUNDLE_PATH, FIXTURES + +import typedstandards +from typedstandards import sidecar +from typedstandards._sidecar import COMMITMENT_VIEW_FIELDS + + +def load(name: str) -> dict[str, Any]: + return json.loads((FIXTURES / name).read_text(encoding="utf-8")) + + +def active_view() -> dict[str, Any]: + return typedstandards.view(load("record-active.signed.json"), visibility="public", title="Example analysis") + + +def withdrawn_view() -> dict[str, Any]: + return typedstandards.view( + load("record-withdrawn.signed.json"), + visibility="public", + attestations=[load("record-withdrawn.withdrawal.json")], + title="Example claim", + ) + + +def _expected(view: dict[str, Any]) -> dict[str, Any]: + return {k: v for k, v in view.items() if k not in {"package", "trustRegistry"}} + + +@pytest.mark.parametrize("make", [active_view, withdrawn_view], ids=["active", "withdrawn"]) +def test_writes_the_commitment_view_fields(tmp_path: Path, make: Any) -> None: + view = make() + assert "package" in view # what view prints: the §8.8.1 view plus the inline package + path = sidecar(view, tmp_path / "analysis.ipynb") + assert path == tmp_path / "analysis.ipynb.record.yaml" + loaded = yaml.safe_load(path.read_text(encoding="utf-8")) + + assert "package" not in loaded and "trustRegistry" not in loaded + assert set(loaded) <= set(COMMITMENT_VIEW_FIELDS) + # Every field view printed besides the two inlined ones, in view's order, equal to the JSON. + assert list(loaded) == [k for k in view if k not in {"package", "trustRegistry"}] + for key in loaded: + assert loaded[key] == view[key], key + assert loaded == _expected(view) + + +def test_the_withdrawn_view_keeps_its_lifecycle(tmp_path: Path) -> None: + view = withdrawn_view() + loaded = yaml.safe_load(sidecar(view, tmp_path / "claim.md").read_text(encoding="utf-8")) + assert loaded["lifecycle"]["status"] == "withdrawn" + assert loaded["lifecycleAttestations"] == view["lifecycleAttestations"] + + +def test_strings_stay_strings(tmp_path: Path) -> None: + view = active_view() + loaded = yaml.safe_load(sidecar(view, tmp_path / "analysis.ipynb").read_text(encoding="utf-8")) + assert loaded["protocolVersion"] == "0.1.0" + assert isinstance(loaded["protocolVersion"], str) + assert isinstance(loaded["packageHash"], str) + + tricky = [ + "0.1.0", "1.0", "1e3", "0x1F", "0o17", "123", "-0", ".inf", "NaN", "yes", "no", "on", "off", + "true", "False", "null", "~", "", "2026-10-03", "2026-10-03T12:30:00.000Z", "12:30:00", "1_000", + "#comment", "key: value", "- item", "café 実験", " leading space", "line\nbreak", + ] # fmt: skip + # At the top level, where a field such as subjectTitle is a bare string ... + for i, value in enumerate(tricky): + top = yaml.safe_load(sidecar({**view, "subjectTitle": value}, tmp_path / f"t{i}.ipynb").read_text("utf-8")) + assert top["subjectTitle"] == value and type(top["subjectTitle"]) is str, repr(value) + # ... and nested, in lists and as mapping keys. + view = {**view, "subjectTitle": tricky, "subjectSummary": {t: t for t in tricky if t}} + loaded = yaml.safe_load(sidecar(view, tmp_path / "tricky.ipynb").read_text(encoding="utf-8")) + assert loaded["subjectTitle"] == tricky + assert all(type(v) is str for v in loaded["subjectTitle"]) + assert loaded["subjectSummary"] == {t: t for t in tricky if t} + + +def test_unicode_is_written_as_text(tmp_path: Path) -> None: + view = {**active_view(), "subjectTitle": "Café 実験"} + text = sidecar(view, tmp_path / "analysis.ipynb").read_text(encoding="utf-8") + assert "subjectTitle: Café 実験\n" in text + + +def test_a_served_bundle_drops_trust_registry(tmp_path: Path) -> None: + """host-core's bundle (the template's, a P1 fixture) carries trustRegistry; the sidecar does not.""" + bundle = json.loads(BUNDLE_PATH.read_text(encoding="utf-8")) + assert "trustRegistry" in bundle and "package" in bundle + loaded = yaml.safe_load(sidecar(BUNDLE_PATH, tmp_path / "first-note.md").read_text(encoding="utf-8")) + assert loaded == _expected(bundle) + assert "trustRegistry" not in loaded + + +def test_directory_places_the_file(tmp_path: Path) -> None: + out = tmp_path / "out" + out.mkdir() + path = sidecar(active_view(), "notebooks/analysis.ipynb", directory=out) + assert path == out / "analysis.ipynb.record.yaml" + assert path.is_file() + + +def test_what_sign_printed_is_refused(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="what view printed"): + sidecar(load("record-active.signed.json"), tmp_path / "analysis.ipynb") diff --git a/uv.lock b/uv.lock index 8cef329..9d48311 100644 --- a/uv.lock +++ b/uv.lock @@ -15,6 +15,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "certifi" version = "2026.7.22" @@ -24,6 +33,113 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -33,6 +149,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "docutils" +version = "0.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823, upload-time = "2026-05-27T17:41:06.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.22.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/a4/9473c7c3b87009d9c1d74034e4a0f6a35ff0d42dd0f9866d0c3ec4e9217b/fastjsonschema-2.22.2.tar.gz", hash = "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf", size = 385171, upload-time = "2026-08-15T19:47:08.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/82/2755c7c982086f00d4dab85bc120ec35045a9fc2191893a6ce79afe94443/fastjsonschema-2.22.2-py3-none-any.whl", hash = "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4", size = 27413, upload-time = "2026-08-15T19:47:04.406Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -88,6 +222,294 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jedi" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "loro" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/83/1dd9181d4b3abfa56c2a7e677a18ae209b38e754a1503e4007adfde47c72/loro-1.16.2.tar.gz", hash = "sha256:b3e30b8d9aadc600662acd31e90ae4da9b84c98900225aa044d4c28c140a12ce", size = 70470, upload-time = "2026-09-21T10:53:40.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/6e/44371a9a018eaaac21939329bca255d4001d33a3eab81f14ad2d68ad0e18/loro-1.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6a0aa2d8a0bf0647cda70c6572a360f17278426ee2d084ad4b3b6319c8388fb2", size = 3427188, upload-time = "2026-09-21T10:50:53.274Z" }, + { url = "https://files.pythonhosted.org/packages/40/8b/52986277e7a7fed3ef3d053e7cbc6ea20cd6c719306f778707f6abb4086f/loro-1.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f51de00868e1b678468ba74461601daae58dad94adba07efd063b6a3e1f93cd", size = 3227346, upload-time = "2026-09-21T10:50:34.519Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/6874108f025b14861c62e608734d77d70a748f77829a4eb268cb1208f5a3/loro-1.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4108ecfb2a256aa749bb4ffcf50b9579b331f89396becafb5e62d86e9bbd5c", size = 3559599, upload-time = "2026-09-21T10:47:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/efd596d00cadb820b7f14ebb2ed9e2fa9982d16f8ca35e71cfe90aa0b912/loro-1.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed97dc8d2ac641f245ce4705c6ffb8fced9395f1ed5d2eb20b65718dfe1bc57e", size = 3639566, upload-time = "2026-09-21T10:47:51.96Z" }, + { url = "https://files.pythonhosted.org/packages/93/ee/4cb3cf1f3bf45277dde07c342b5f9be098b3efd3b4f08dd077b1fcc2689d/loro-1.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03e24f219c823960aca6e2c466977e8ce26df91868b3d3fe8eb164768e0b038e", size = 4010938, upload-time = "2026-09-21T10:48:28.445Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/ef4f153a08a464c0633d597dc11375a2e08bd837ac2c65fb0743d6a0f4be/loro-1.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b2caec351f753979fcc7a61ab0defa1998c518140e142f6b132fd9b1b4fbc8", size = 3717124, upload-time = "2026-09-21T10:49:05.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/68/2fa5bfad2911616836ef5dd0f23e218d2bdfab1604bbf803558bdfd9b705/loro-1.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8612523ad7196a4522843db9a2ffd6412136a1c0abe4e20a648858c135b96daa", size = 3557218, upload-time = "2026-09-21T10:50:07.805Z" }, + { url = "https://files.pythonhosted.org/packages/4b/11/1f476adb6b439147a412e11b3977f000de6bb44b1645404b4ef57816d0b0/loro-1.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eb30a41d6e35d61523beff7facd031f645d9c20e046e99f4b24398e14e678f99", size = 3944735, upload-time = "2026-09-21T10:49:40.756Z" }, + { url = "https://files.pythonhosted.org/packages/44/42/1f9416dd7f7a932dd0d2efc7823e5a7ef0148df9491bfa663ffc298134ab/loro-1.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:19f6501cd4eb4120641e4ad8c84cabd6f4fd91eef216ea632293ef232e8d0c73", size = 3737187, upload-time = "2026-09-21T10:51:11.467Z" }, + { url = "https://files.pythonhosted.org/packages/74/33/d7a378ce939274cf0c7f6310f95da5c410046d2b51239ef34a5824b6e16f/loro-1.16.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1ffdda0dd25704583767e97bff2e379648a09f04a134ab5d326db7f7013d3a7b", size = 3911198, upload-time = "2026-09-21T10:51:48.352Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e9/db0f75232d557a51c8191d494984aa38b913d78091e52cc40fa5588f3db2/loro-1.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0420c03f1c73c2e99b810ad42833b8652520b089f03e813d29e19db53a5c2e8a", size = 3962982, upload-time = "2026-09-21T10:52:25.037Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/6f3c0e8f552c610cba91c689421c7201b654d921b513c32f9047ee16dfb6/loro-1.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df0ef855397015fb5d819d07f12cd7cd557a5b96b7cdfd1ac5197cf3c2f66c9a", size = 3876810, upload-time = "2026-09-21T10:53:03.39Z" }, + { url = "https://files.pythonhosted.org/packages/53/72/37b6b38ea743f7651e557829292b83e1330f44441b8eee63964f1efbdd81/loro-1.16.2-cp311-cp311-win32.whl", hash = "sha256:62b40e6fae0aa5c1bf5e073f1df0a6acd9f880ad2fd56cbcd7a2135589fe0fea", size = 2950225, upload-time = "2026-09-21T10:54:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/99/56/390098d9c332bc33c18f30bdf546080c3ce53073fdb387b6ec56d0a34ee0/loro-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:8181a4c1b92cb35bab2b322fbe0d8b340cd21def474b1484c34c458f324bfebf", size = 3181244, upload-time = "2026-09-21T10:53:43.565Z" }, + { url = "https://files.pythonhosted.org/packages/14/10/67676c4f0b247b340179a68fad7bb5d981c2e54e7f4682513d9250d90c85/loro-1.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:84ef4084a7be2fc3391d530ef302840459ee23dd95ef77dd638b3c8fe24c863f", size = 3408380, upload-time = "2026-09-21T10:50:54.686Z" }, + { url = "https://files.pythonhosted.org/packages/08/d3/e90f2c9db0b2420a4501eea1898d10769e82b67787e8a505eca176d73dc4/loro-1.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3e158f81a2227b9ccd9b66139c098b79ff0eccc4825f2977a8ecf6247dfd4983", size = 3215934, upload-time = "2026-09-21T10:50:36.555Z" }, + { url = "https://files.pythonhosted.org/packages/54/cc/4226e8156d80640afd74d1b3053c62adbd307d080a21d2a4256156a3ddc0/loro-1.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad39c737e42007b9d1e63d4936798a444f5fbd9dce76a1fdc41096900a8ae58c", size = 3560484, upload-time = "2026-09-21T10:47:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/68/a6/3aca16bad28189450fff14aed1f7a5fec2577bfba95778aa1a580f514df3/loro-1.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c09f50be03b526c9823bfe3e2cc02f42bd53c1d4c6d81fe9b22035b12399594", size = 3636885, upload-time = "2026-09-21T10:47:53.506Z" }, + { url = "https://files.pythonhosted.org/packages/72/35/581373a232c53e6e205600e0d065c42824e3106aea35cb0990ad9bd6edd9/loro-1.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad8b88e00a1e7f9a8348935c73abb33dbc573af7d1bd542f180804a39e8f7988", size = 4011097, upload-time = "2026-09-21T10:48:29.927Z" }, + { url = "https://files.pythonhosted.org/packages/37/ee/b55a0971765f129582ae7862ed9f30981f71f93cf78b7cd401e56b3d3091/loro-1.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5ef353110b2e1c2cc332e66aa07704b8571960523117841cfc0f5711dc4310c", size = 3721604, upload-time = "2026-09-21T10:49:06.991Z" }, + { url = "https://files.pythonhosted.org/packages/6c/07/f539e4d5dcf252430af9c4b76a27be756c3dbcb400ebc0e33578fa8552b6/loro-1.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d42620092340597fc9b8c76ec76bed92de1326b7c9418a49a8909371b598803f", size = 3558887, upload-time = "2026-09-21T10:50:09.16Z" }, + { url = "https://files.pythonhosted.org/packages/79/18/093a00b64aedd5d7ce33e75b88f9f25278baf46fffbaf54f60e6495411c7/loro-1.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fd2c787299b6332b308f543253f4bb2885536cb80799799168836936cfa619", size = 3951738, upload-time = "2026-09-21T10:49:42.201Z" }, + { url = "https://files.pythonhosted.org/packages/29/6e/bfb0c49f06f99b7acbe90872f327b49ba7ca5a7b22d777f528aab67bf94c/loro-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b19e18c5c0deefded5b1f41d15a34c3c3dd8e89491c1f086cada9e9b6ab5942c", size = 3736893, upload-time = "2026-09-21T10:51:13.212Z" }, + { url = "https://files.pythonhosted.org/packages/93/cc/0f08597cdd6124dbbb42330dda2e25e6e7231df3e809c212dec2c419e3f2/loro-1.16.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7af386703e10df2d9613479b2d558225063023f726902cefd39a799a404ee69a", size = 3909291, upload-time = "2026-09-21T10:51:49.644Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/3ce1099cf71e882906e01d476864459956c136a41a561ce73a34b0a02818/loro-1.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ef6d5ca7f32aa26d5c4f61f794ed0343b893f3e6034bea760e9ece1d526ba23", size = 3967519, upload-time = "2026-09-21T10:52:26.537Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/9453c9381cc9002cf2eff160c705aa3404711c909d162e3585640223c3fe/loro-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4ac4641cacf192d59f0daa92a5cbea101f50a5d7c44c0f466ad98f4571dd1d53", size = 3877159, upload-time = "2026-09-21T10:53:04.999Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/9b078f371c6372667253fa82d15184dfcfcbdf1f478d7e57f3a20abd9ef7/loro-1.16.2-cp312-cp312-win32.whl", hash = "sha256:5a29103d094e592df751cb35b1f3197586f1fdda9cdcf707b80bedbe5394ef56", size = 2950327, upload-time = "2026-09-21T10:54:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/34/0d/6b8140ff5fa6d350548b4e088d2303f667ace864b189da441c9e1dcf6593/loro-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:503af039fe68fff1874cfbac86801784f541d00cce61284f15f28fc0ba8bbfd2", size = 3183597, upload-time = "2026-09-21T10:53:45.227Z" }, + { url = "https://files.pythonhosted.org/packages/77/85/21ea0adba99b7a890318ea69a31f72eafc411449b9024f845f07793d6e57/loro-1.16.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f10995db58779d3cfc5f233f84fca09fac6fc20bc5b7db71b4afe7b366f41b24", size = 3408650, upload-time = "2026-09-21T10:50:56.222Z" }, + { url = "https://files.pythonhosted.org/packages/43/05/c6f98a5638373ca2b967194fe1c778cbd7bdd715adcefb91b79f325c0191/loro-1.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:455771cfda2c84d08861ab64f0fb541d67e32193ac82a83a9dc8ea6e711e79ce", size = 3215484, upload-time = "2026-09-21T10:50:38.224Z" }, + { url = "https://files.pythonhosted.org/packages/fb/fa/94c321f615fe2cc0178d466e8c92c00c77615b89994c5378335b52f3cb90/loro-1.16.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11e29b820fd88f86e18e4f05055db724fd0a68372356581f4318cb4b9e368f62", size = 3561932, upload-time = "2026-09-21T10:47:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/30/1a/3b73c87eed13667fc04bf4627b628e8e38fdcf9ed5898b9c122f0aaaa3d5/loro-1.16.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0738bd6d2d3149a7c2d9496abbdcb6d5105a70d8d61e23a6536d253d7fadae", size = 3636574, upload-time = "2026-09-21T10:47:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/12/eb/776be0d65ea35a83d52d84817ef04ee62e9cb496285f7d13582baf9b065a/loro-1.16.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d277aa9422ec314a1eb96503a827138dbc0e5650301304904fb9b35eb3605911", size = 4011370, upload-time = "2026-09-21T10:48:31.564Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/6aa57039390bd0fc39c33cd025d524ca8927bcfe1540f7d9b9b31e1001e3/loro-1.16.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d812435e3a26b5a7b1417bf1a4fd7c9b951adb9817b407b1a5f935f2829617a", size = 3722827, upload-time = "2026-09-21T10:49:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/18/f4/dc9ec2b7c347a3b7694bbb4412e58a650f5ad79a7ee72a70277f31cd98b5/loro-1.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6c069242a3a7128a6c929c7bc87d730644bac01c83fc29141755b014621772", size = 3558981, upload-time = "2026-09-21T10:50:10.469Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a9/17658f6ec319e195ae86d38c11d0478404aac0f13296cace2fc8002e0786/loro-1.16.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:adec0565eecc806ee9dc6470ca26b784c0d67270129ba8611f546552140629be", size = 3950831, upload-time = "2026-09-21T10:49:43.565Z" }, + { url = "https://files.pythonhosted.org/packages/48/f1/e055e5a8f047f404425399ef09431bea6bf7404aae5fd120d20579a241ae/loro-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:acb942480d16226a538dd63dc1fbe532ae66dd1fb9e8800ed454bc1e6a12cda8", size = 3738048, upload-time = "2026-09-21T10:51:14.845Z" }, + { url = "https://files.pythonhosted.org/packages/58/fd/e84706fde319f217dab5e6b5a53d88619a22f9b32b8cddb52ef1cf49c561/loro-1.16.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e0c1108c204fef6db8d1107a4c2c4367ddc6759c6b3428c904d5a283d0e9f72", size = 3909244, upload-time = "2026-09-21T10:51:51.163Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ee/6cd5e8b3ef105bd28d005747e3a2846bd2a76dfd49b710dec7279af4a0ee/loro-1.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:aa72e367432153907849b4f290ce287932563b73818b83f8aa3d5f2a2142af02", size = 3967937, upload-time = "2026-09-21T10:52:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/fa/db/979b4a54007caee21885b21ef319d465671242e57551ea5cbedb1d327722/loro-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:32d7979ef4bd2f4dff73cca1d8b2634e035bbaee590e167570866721623edfeb", size = 3878292, upload-time = "2026-09-21T10:53:06.785Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/186255c8859f2c27e7bf94ef6d80d56e9a30fb8ecf3131b3d8a6a027e783/loro-1.16.2-cp313-cp313-win32.whl", hash = "sha256:4eaf8e500d4c1e9da8edbec6ded28758f44d59e0f923778301480ead59504e97", size = 2949712, upload-time = "2026-09-21T10:54:08.75Z" }, + { url = "https://files.pythonhosted.org/packages/b9/76/a4abc1bdb824b130508eab9ffaab7e99eedc2325070300713adcae6a46be/loro-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:c05c11ad43c57190e7d912a570328f7294a6b569ccbff6eedb788414710e4245", size = 3183368, upload-time = "2026-09-21T10:53:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/f9ae6e61722041e062245b3f2d9464fc2eb28631b688ca2589b6321f48e7/loro-1.16.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a02cc0c788a070a8b6b8c111642fce80ed0f9bc52cfb8361f3a671ed29880bf7", size = 3388067, upload-time = "2026-09-21T10:50:57.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/43/7d69e5faf912c51fe03c133ccb847fa7ed6f003a742d1959247c862e144e/loro-1.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c72a7d1e6a05c9630f75de9a9a78231c80eb1ad2a3c9f6554ef287fb3d57aa8", size = 3202677, upload-time = "2026-09-21T10:50:39.61Z" }, + { url = "https://files.pythonhosted.org/packages/52/d7/c19d7d6e984cae8a2a3a6171d16565e39fb0eb5fdeccd507af5ba1946e05/loro-1.16.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a523e5c3a7b321b9664ac9ebf992b8f872f53280d2735e2340dbd77f7cceb8d", size = 3551977, upload-time = "2026-09-21T10:47:19.047Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/33b86554151228fc562e069eb9cd7246466864ca0d22242515029ab802dd/loro-1.16.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf8f3d7fbbbe8921eb0a89dea6fbbef4bec361aaa03541a957e3778a820c72ec", size = 3615097, upload-time = "2026-09-21T10:47:56.763Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/bd12570b97cc9afa306be444ef45322433c1a630b8816923c497da85d182/loro-1.16.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6276a87a64904a568f7f846d568a187fcd125213f4f1a3f756ab3f9350dc9aac", size = 4007066, upload-time = "2026-09-21T10:48:32.928Z" }, + { url = "https://files.pythonhosted.org/packages/61/11/334982f10a2ff7f66b1d91157ed502c5ebffff7d1f6cbd7199ee9a722d8d/loro-1.16.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0ada36b1965a81d50d7dc9e35df42fd84077ea8192b072f9286822fa53f74af", size = 3714840, upload-time = "2026-09-21T10:49:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f3/c5f18b9883bf30703b9d5e02f878e4c81960cf7c20a4c6502b669eb00e39/loro-1.16.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb3aab12b9e7e4b85141a47fc90183a1cb0fe6549f6cd33666827b5da470721c", size = 3541250, upload-time = "2026-09-21T10:50:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/9e/72/0bcf0d64ff5e5071667e2a059a81223586ffee493c116e6975bc5c95d429/loro-1.16.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0822ec78a2cb2087203adc0f7c4c105de5c9ab31a735efdc6e0d64c05d8e30b", size = 3935828, upload-time = "2026-09-21T10:49:45.12Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f0/0d2be34b9ddfa38f0990e17456a91e5cb39d1cf740dbe302063999bfa917/loro-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:333778715b84d9d46f547d4c4f53d8f02739f3a1199fed172f345d472002b642", size = 3726581, upload-time = "2026-09-21T10:51:16.402Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a2/a924c60fc045f35ffab0f3b05d84cc62787b85ed8177b92b724c15d2076a/loro-1.16.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ecaa21e06a3707cdb7066b8bd5e9752aff4a2196bd6154a55863ab26185ae718", size = 3889480, upload-time = "2026-09-21T10:51:52.908Z" }, + { url = "https://files.pythonhosted.org/packages/b9/39/9aedb1538a672b5d9867d080ce23f2af169f36bc2302e6a96fb6d96a1988/loro-1.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:671f5270d9520eb157de9be16365c29e921919e9bc622f15e7ae7d28196b9209", size = 3959009, upload-time = "2026-09-21T10:52:29.843Z" }, + { url = "https://files.pythonhosted.org/packages/8f/45/6305eae57f5b55fcea8334a4fbb828440e4a8abe532889f2e05872f4e103/loro-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e3dc7188c2a68a38350a98838d97e216f93b803d2a93fe501111c08d219095a", size = 3861624, upload-time = "2026-09-21T10:53:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/f2/25/e2eb3cc092d64ba5e579d6c217082a10c2d55090b9ac30c0a1728672579e/loro-1.16.2-cp314-cp314-win32.whl", hash = "sha256:c6d28f85e19ac88ecee4fd535999d71196d1a3fba2c66dae54b6168687419c1d", size = 2937950, upload-time = "2026-09-21T10:54:10.436Z" }, + { url = "https://files.pythonhosted.org/packages/6e/40/6557fddb37ad3adfb36b60350d930e1eb69412e7a5328c46108fdae67cf1/loro-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:e420efe54720a7a6b2c9f421d0cc5c56c01b09351162a452d33d377f2ee87778", size = 3162432, upload-time = "2026-09-21T10:53:48.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/56abbe5452186e8546ca62f4f508016550202e1551d136eebab45090317d/loro-1.16.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46f2ff5434616e6d5b118dad992804ed1d4e8dfefe1a4c4c64273b0bd3e5616f", size = 3553092, upload-time = "2026-09-21T10:47:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/a2/19/fbf6efa486978407882395062b65687c2074d831c3f5d1211dfdac63ccb5/loro-1.16.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c551daf053fa5ca594db147c8b2044b57144595111b0f57a672edf54b5756c4", size = 3617321, upload-time = "2026-09-21T10:47:58.106Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/fafc3df7cf5ef142c77327c9fcfde24a9825e31fa87cc20bbf164bf3a9e9/loro-1.16.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4931866c3a6bb09715c55ce9ba01062a678f44d6478cf1b1c163222f954f2f98", size = 4004449, upload-time = "2026-09-21T10:48:34.467Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/e9c6db12d1daa3df1cd78ccc4232bb20a2e9b09d702c7e57fc8c6c60775b/loro-1.16.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:93f62f2939544319d2bac1292c788e473ba5aac946ac8f03e0a19ebe46ee882b", size = 3709917, upload-time = "2026-09-21T10:49:11.304Z" }, + { url = "https://files.pythonhosted.org/packages/90/4f/26f496f65fd053ee527a8da720138b52a521404fcaa58110c4329f7b68e4/loro-1.16.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1298a0f27e22589df53ffbc6178785965bf86cabd0a7fdf839b112c37f034c4d", size = 3536458, upload-time = "2026-09-21T10:50:13.773Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/0b5760638b48c5cd7c16831797b151697c08fb10b7f629803efc655f683e/loro-1.16.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4281a93df46ec02583365b7403e703787e0a794303495b7b61d6a121090bed15", size = 3940244, upload-time = "2026-09-21T10:49:46.804Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/dae1e99592b225b9b757059c15c5351fd0f165ce2309631d73a534cf4428/loro-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:505f78453cda745dd1ef6e9e13cce11208a546730e45989ae4bccabee5e72697", size = 3728368, upload-time = "2026-09-21T10:51:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/54/7f/59737c7bee98d07b3057bdd3f8824e42347d3846d235336fedaf3980fd07/loro-1.16.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf2dc9dee8b12aa87585decdc11f337ff7f67a31629c3cda00094c894e3f69ff", size = 3889805, upload-time = "2026-09-21T10:51:54.693Z" }, + { url = "https://files.pythonhosted.org/packages/1b/79/2602758cea0d22b07c7b63aa7277d64a10d2017fe7d2b6349811b64b1d50/loro-1.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6dc4e8ed7bcdbceb784fc4976428016fa572177f7757503d489d3aee8b8d31f9", size = 3961692, upload-time = "2026-09-21T10:52:31.548Z" }, + { url = "https://files.pythonhosted.org/packages/50/8e/df588b63fe352bf6ea9154df1417b4bb061237068beaba24bf7b2b8a5435/loro-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d95c5223f23f6be9cd1439ab5b65ad9e7e23ca6b294f8ac14af982bed36d984a", size = 3860841, upload-time = "2026-09-21T10:53:10.065Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/a7911d76222b93e651a6799aef38b1d2f43324cf59b906d108bfaf50aa94/loro-1.16.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a17e658f7e1d9129ed4096032e5909536c0c88eb603f35144cfdd774751514", size = 3557159, upload-time = "2026-09-21T10:47:27.354Z" }, + { url = "https://files.pythonhosted.org/packages/e8/33/d6a35f625f7415b9e4e0256e11401138541e762d270e801b14a3a7126da4/loro-1.16.2-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02bd9d884831bc1b3da64f96f8f2291e1df4c0e58f446243e69cfe66a6010219", size = 3632082, upload-time = "2026-09-21T10:48:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/acfbef910de4bc58bc1b54a34a28c66812cd1426ba0b0e26fdae7f39d465/loro-1.16.2-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7247c8306e875efab257c657e646a2e30cced4e2a8a5dbe70410904e8e0bf24c", size = 4006389, upload-time = "2026-09-21T10:48:40.252Z" }, + { url = "https://files.pythonhosted.org/packages/19/ef/6eb98f1d81336fcd6b59d854dd57eddd7da9b0cf6f5795b4f052272c2902/loro-1.16.2-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a3eed5ebcb63f3693e3233951518d9a29ff62b6f2c1647d38c76bc0525c1e63", size = 3718140, upload-time = "2026-09-21T10:49:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/d3/89/a8c2ea75aae950bff79689c8166548b6c2ea16dd28fff7a7e6f3789906c3/loro-1.16.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:132ff1ac782a4a4beeb49029d362a3f7991015831ecb3ae2d469e502338cb246", size = 3552939, upload-time = "2026-09-21T10:50:16.534Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fa/35e3affe208ae469d2bc904af0fbaedd1de9c454f8707312bc456d048f5a/loro-1.16.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1b550955298e0499c5e3983dec50e61db93fe150e21d72d7ef4e3ce58c7be1e", size = 3943571, upload-time = "2026-09-21T10:49:49.913Z" }, + { url = "https://files.pythonhosted.org/packages/4e/82/cd3740efa1c2457a1d2441692a819a6b4c2d9cae6e78534f9c3a181969ac/loro-1.16.2-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:67900839e0a1330e4f732e3e8a3d95a594b72efd7f725011b2c04a596b30181a", size = 3732402, upload-time = "2026-09-21T10:51:23.799Z" }, + { url = "https://files.pythonhosted.org/packages/fc/80/40480acbdc79545ba9611f2b049774112cc255aaee6856f6552305caaf9c/loro-1.16.2-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:ef8407ccedb902ea92f01e819e0db88ed5b47e474a5b8fc92cea7f12299c1f8a", size = 3906160, upload-time = "2026-09-21T10:52:01.313Z" }, + { url = "https://files.pythonhosted.org/packages/90/b5/7137578bf9bb66733dfae665b192e7e947a32822ee76d508ada3408a84b9/loro-1.16.2-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:69eeb81a7534fd1302ff64dd3a8cb8c8efddffe7188ec71e016e8a0c9efee93d", size = 3964132, upload-time = "2026-09-21T10:52:38.725Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/70713536ab052d6038556bd67fd6c3d4e15e9a35987147eeadc14806f87d/loro-1.16.2-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:60cf79bc63725993b694b754323d758887dae02d4c98f5cf95258352c28a9ac2", size = 3873354, upload-time = "2026-09-21T10:53:16.801Z" }, +] + +[[package]] +name = "marimo" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform != 'emscripten'" }, + { name = "docutils" }, + { name = "itsdangerous" }, + { name = "jedi" }, + { name = "loro", marker = "sys_platform != 'android' and sys_platform != 'emscripten'" }, + { name = "markdown" }, + { name = "msgspec" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "psutil", marker = "sys_platform != 'android' and sys_platform != 'emscripten'" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "pyzmq", marker = "python_full_version < '3.15' and sys_platform != 'emscripten'" }, + { name = "starlette", marker = "sys_platform != 'emscripten'" }, + { name = "tomlkit" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, + { name = "websockets", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/7b/29895b0d747758da46a051f57c594634ab02f3993d5eab2f2e49ce4f144f/marimo-0.25.1.tar.gz", hash = "sha256:934db2ceb6c2b8c9ae3514a7a22e6b22ecf54fabea0b6138aed1980f8f1536ff", size = 50186739, upload-time = "2026-10-01T21:01:28.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/e6/695f462fd473981325146edde01c12390413ebaa33c1f0b49191d6b55f86/marimo-0.25.1-py3-none-any.whl", hash = "sha256:dec4a87bc5db16b85181ba4880e67bb55485912b7ef26748a0bbf052c59203fd", size = 50850300, upload-time = "2026-10-01T21:01:24.577Z" }, +] + +[[package]] +name = "markdown" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/4f/700155c8c20d9e655dd0732b5fc3c7614f291b9148da271d7388e50bf774/markdown-3.11.tar.gz", hash = "sha256:180224db6aed87ba9ce1f2781ebcd5826253de8ff637112090e24b84502bbf9f", size = 485623, upload-time = "2026-09-25T13:46:23.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/1e/32971905a7ab47f8b66866ed949fa48b104ba1c4a6fa57794c4f2c4b2cb8/markdown-3.11-py3-none-any.whl", hash = "sha256:cd6c89e7eb308c8b332ed673215a52d208a43f8bacc030b1419376129408719e", size = 111296, upload-time = "2026-09-25T13:46:22.163Z" }, +] + +[[package]] +name = "msgspec" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/e6/6dcf9306ff3c5e486578f3bf29ed11dfbdbbc2a8bf0caf7e07d392887fda/msgspec-0.22.0.tar.gz", hash = "sha256:0a13624a4969159fe35d8c2a3d377b2b61bbd8585e327440d5e52725affcce38", size = 343188, upload-time = "2026-09-29T14:14:11.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/22/45c17acb1a85360b10afb95f66777f76bc2634993c66db8b7833832bd343/msgspec-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb1e129b81ac8fcf9ec649b081c6c8da1c7ea6f87cab336d46386abc2cd855c1", size = 198231, upload-time = "2026-09-29T14:12:23.016Z" }, + { url = "https://files.pythonhosted.org/packages/34/79/1cf725694125051e866066d74e6199206838d1465cbfc35081dc29b6e366/msgspec-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dce29a04966e31abf9b83b697c6d672486526dc5d03fcd6970cb56d5dc1fbeea", size = 190911, upload-time = "2026-09-29T14:12:24.636Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b2/e0ace038031a2988aa2e85c431c4d7aef734fbba4749ace6bc5bf310b769/msgspec-0.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b962000e11dd34fb210a5a2c57a8a62b2d92b381c8cb3b05c075a83e38f8d645", size = 220343, upload-time = "2026-09-29T14:12:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/16ddb09185d79dc00177994cf0bdb1cd8e5cc44a1d1bfba61bdda5f382cb/msgspec-0.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6db3806b3b76ca78064255eac6fa101a8a64fe6f698d80fbaf81fdfa21217d4", size = 225251, upload-time = "2026-09-29T14:12:27.559Z" }, + { url = "https://files.pythonhosted.org/packages/16/c2/a6af0d38fb0e72f02851ed084c4b8175140cfaf3eaf48b38da0c3941db26/msgspec-0.22.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a88d939d3fe4b8c7314645ebcd6e86c8c8a512ea7820d6550355973e803bc0f1", size = 233488, upload-time = "2026-09-29T14:12:28.996Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9b/b1c4208cdf487e2ba7af145f721b279444ff76af05a9f8fce992ed0588ee/msgspec-0.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b31746da07cba0e330c6433a94a4699ad77d3aeb9638d1a320a7686b69f6249", size = 225688, upload-time = "2026-09-29T14:12:30.351Z" }, + { url = "https://files.pythonhosted.org/packages/83/54/b9240d908674ef7c41d02cb909731ad6d9931c23bd6a27d8d10776c6f964/msgspec-0.22.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:6ae370f92f3517f0e6f209ba7cc649c957b444868439197e046be07154667551", size = 234250, upload-time = "2026-09-29T14:12:31.887Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/d498798aaab3bd191a33955de47b40f07fae7667d86a33b705443a7e9491/msgspec-0.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a696f23f7c1ffb31fae308502e01a3965c3891d5c400f01d0d1096dbe77519e", size = 228337, upload-time = "2026-09-29T14:12:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/fa/51/5e9ae5a5ddc254e15435749328161e95598750e5df644bb00fa9e2297122/msgspec-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:024138c51afd335d0b4dce401be33902caafac2b64f8c9f2509a378986175d98", size = 190962, upload-time = "2026-09-29T14:12:34.847Z" }, + { url = "https://files.pythonhosted.org/packages/12/38/fb64a18543bcbebc53a375cb00b1c93bf264a0b6c7bbe9e38b37cc5f0768/msgspec-0.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:4600dbec738ed74e4c9bd35503e84701200ea7db344cfdeda80677b3ee53eb64", size = 189458, upload-time = "2026-09-29T14:12:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/a4/87/3e017dca361d09ed1cd09dc981a6df21b32e830fbec3470f7486d38b6be5/msgspec-0.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab1e9e7531e353653b906cdd12a0220cc288a1e8e3436aabc65f4508d91b14d9", size = 201301, upload-time = "2026-09-29T14:12:38.048Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/109165edaafb895668d87177972a32ade9126a54f3736123d8e44be9096d/msgspec-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b60b43425a47eb9cfe987f6874e354ca7c760e58e295b4e2273ff03574df28a1", size = 193044, upload-time = "2026-09-29T14:12:39.46Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/65de05f8804492f76ea121b21a125cdf1d97ec461c677bfa0ba354d6fbdd/msgspec-0.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5a169b5b03f0f2c7a296c002647db1dab75d2cd501bca34e32b71cab0261b56", size = 224035, upload-time = "2026-09-29T14:12:40.876Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cc/aa1a47f8c92280d37498a5ea56a2a36606d034383e3e6472d64cbb56cf85/msgspec-0.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c401861c5bb3a57f7d6423ea7ed4352cd57aa3f04f4fbe9f3e3e4564a10f08", size = 230377, upload-time = "2026-09-29T14:12:42.796Z" }, + { url = "https://files.pythonhosted.org/packages/61/50/f8bcdb3d613a4a4b92704297a12eba5c985cf572a64ee1a004d265759c69/msgspec-0.22.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08826f5e5b0fa2f7a88592c396a243cfcc63d37e19f9d4fbe3b3f1be2fbdc404", size = 237390, upload-time = "2026-09-29T14:12:44.282Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8a/473fa423f8fdd1b810b8652594323d7301df6920b62844d860daa0feff34/msgspec-0.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21460f54cee9208239b1a8421fdf25bffc77293e1daba88f585711ad839b9758", size = 227733, upload-time = "2026-09-29T14:12:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/03/1d/272ce23adae6c71b3f763aed3ee6e115cccc56124ed8ee0e3e3d2681e2c8/msgspec-0.22.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cfc3d9557de9c806318725b702f3e664db33167bb42892079b693c69893fd33b", size = 236783, upload-time = "2026-09-29T14:12:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/f6/26/29e0b9a8605c8819a3c718158e345a616ac42c092dd7d7ab248c2f2b0a72/msgspec-0.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0b25dcbc108783cb72503ed705b9fbb8c3cb02ee5801923f44b5f038c91cc365", size = 232728, upload-time = "2026-09-29T14:12:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a6/99597c281d716da6c662b48dcc3f734669f716b41d5df2af367dac9e7c21/msgspec-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:6ad64f5c260866b0d543f89f50cee43628989c1433c5de7ce820281fa28a2611", size = 192885, upload-time = "2026-09-29T14:12:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/46/80/85fff923d448b886ec3a85900c578d9367f08dad54fe48879495b4c6d055/msgspec-0.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:0922714feff5300aacd8ecd65fa828317ce4bf5212b3139258c0bfc0253cd80e", size = 191223, upload-time = "2026-09-29T14:12:51.699Z" }, + { url = "https://files.pythonhosted.org/packages/7f/62/5374fba2ede0408f4bd8b9b3a6c8464f8d0ea7ae9a2a064bd81ca492bd1e/msgspec-0.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f13c127a945479bc9db057eb253b8851075c8e1ae07ffc967bfa1c5676203a86", size = 201355, upload-time = "2026-09-29T14:12:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e3/357baa8d2a9164a98dfd7ef9d3a58125df0ed981be909945bdd337be7194/msgspec-0.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5aa24eb475d070ecbbe5b21080fc3ce4b0b76c60de25cfe0c9678d8fb44bb42f", size = 193097, upload-time = "2026-09-29T14:12:54.52Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1b/9cc07718d1dee8ed5e89a265801d565bc0f15ead435ccb198f9c7bf92574/msgspec-0.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:627bfdfe5a4b3d916b3360b30f4cddeee3a084f56593e33527c6872fa8322ff9", size = 224112, upload-time = "2026-09-29T14:12:55.983Z" }, + { url = "https://files.pythonhosted.org/packages/46/64/f33fdfe95aca76601194a7064d14816c7c22c4eccc1b03a5335785895fa3/msgspec-0.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6c310ef83e7e291b01a63298828f848348bb99e84a1098c4b3923c05674d032", size = 230472, upload-time = "2026-09-29T14:12:57.648Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b3/8ceaa9981c230adf43c45a6e8da25da23a381eddc7ed05aeaca1d5e7928b/msgspec-0.22.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c1e76c6bd523141b9c05c2f8a70979cd0efedbd68855a66f292f8892c0b8fc7", size = 237382, upload-time = "2026-09-29T14:12:59.414Z" }, + { url = "https://files.pythonhosted.org/packages/88/a6/7b5c4fb39e0bf2dabc8be923c33c39b07ba769a0ce6f0afbbdfaadb1f2f2/msgspec-0.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc374dedd5f85a5f4de2386dc5f737894ccb8c1ac18e9566ce66fd9839e6285d", size = 227717, upload-time = "2026-09-29T14:13:00.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5b/2334ee638880e756c8bc54a1177bd65877c786433693a43594ef5ecbe2d8/msgspec-0.22.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:feafe612034d49e9144340c0b5168ee4e22c2af4aaa2c1db11ae84e1aac9543b", size = 236781, upload-time = "2026-09-29T14:13:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e5/b4c5323b17ecfce45350695d40fc93e16856db957a53cbcf2f53007d6e12/msgspec-0.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f48317f05312bfdf78248f53933f830f07ab75cc1c813ac3ca4220cb3b5b019", size = 232777, upload-time = "2026-09-29T14:13:04.025Z" }, + { url = "https://files.pythonhosted.org/packages/01/33/e591f9d3d8d6c9cfc02ae95f3e3c44920f2d18050f3f252c244e0f293a0e/msgspec-0.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:0739b068f31f2004a364f97679ba91f2f5ecd6ec2a5b4b890188ab5c57d20672", size = 192829, upload-time = "2026-09-29T14:13:05.519Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cd/a011a5b8732cd781e2ea6da5b38d71ae4a9a329338411d1f008a58f5edbf/msgspec-0.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:508278300dd4efbd21cd3a4b2b016160a5feac98bc880d3673f6c06697baaf62", size = 191258, upload-time = "2026-09-29T14:13:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/53/f9/ac027b35477e6b83bcee32b3d9675b37abfa130f098dd6500fa67d768852/msgspec-0.22.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:221cbcbfa4478152b91d37dcfd4830e2be92773e8139e883f43773450ebacef8", size = 201276, upload-time = "2026-09-29T14:13:08.311Z" }, + { url = "https://files.pythonhosted.org/packages/13/6b/2bffffa31662b1353a62e672442865d51c291ad778352fd490de16361dc6/msgspec-0.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dd9568695911055440d2bb7099ed9098fc181d335daa772d0eb3fe8f31ba4efb", size = 193233, upload-time = "2026-09-29T14:13:09.943Z" }, + { url = "https://files.pythonhosted.org/packages/14/bc/4066416ff6aa918d1ef9295edee0041e4629e4079ad3839bdd8a68fd87f0/msgspec-0.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f039ef5207b847f075a0a43020ee6140cd47505f890e47e157f2deb485c2dc96", size = 225101, upload-time = "2026-09-29T14:13:11.391Z" }, + { url = "https://files.pythonhosted.org/packages/63/ba/a8d390d5bd4c7d9ccde87c95cf071ada934cc9ca2c6af4d3d50b38f2d718/msgspec-0.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4f7e09cceac7dbf4c0761b8ae7df51c55b5df5e9af7aff2c895aac1ebea015", size = 230505, upload-time = "2026-09-29T14:13:12.869Z" }, + { url = "https://files.pythonhosted.org/packages/9c/89/979664fdc913c624ef88a139b40e3a95ddf2a47c89e8b5c4147f69ee9c48/msgspec-0.22.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:614e2c827e0a3f934f3cf0cf4ba65210df8132b75a69a8a1f51bb3b2caf0ac5a", size = 237382, upload-time = "2026-09-29T14:13:14.317Z" }, + { url = "https://files.pythonhosted.org/packages/07/3f/7d44c614376ae008ac6099be5f589b322c4ad44e32c6dbb0edd256215028/msgspec-0.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3689b9dfcc663358ef23ba4299d7460f01108515b041a7d30d05908ac9c32f", size = 228962, upload-time = "2026-09-29T14:13:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/0b/59/bf8504e6f63f6769d01fb66f8bd856cf0ed39a07fde354f440d711640054/msgspec-0.22.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2f950239ff1fc7322c6f9634807310265149cb168270d3ddcdda5b6ada13a28", size = 236691, upload-time = "2026-09-29T14:13:17.195Z" }, + { url = "https://files.pythonhosted.org/packages/2b/40/5a9d2bde12af16a22ddbf371990a81d3e3c0dcd4bb4ef3b3f9616b033c14/msgspec-0.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c789b5ccd07c0a3c09767108ee06e089b2875f2309a4569c2648f30a8d31dfa", size = 232750, upload-time = "2026-09-29T14:13:18.691Z" }, + { url = "https://files.pythonhosted.org/packages/75/5d/c0e6bdb81a87f6bd56a663a330c271af7670490c80d8d635d9fa21ad1adf/msgspec-0.22.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a66b1766311e42371e509c996c3933b161c7ae0eabdf361af5316dec197e1022", size = 136814, upload-time = "2026-09-29T14:13:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c0/b0cfc6d33608e5ea8871f3be31f9146c56699e737a7d8862bf018484f278/msgspec-0.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:749899563d26b211379f142b8ffd7e2d7da149a51717798f0ce994dce50324f0", size = 197097, upload-time = "2026-09-29T14:13:21.869Z" }, + { url = "https://files.pythonhosted.org/packages/42/1f/571f7fe7c725380605d680fc4c0084212b23d2dfcf6be0f2277f14462c56/msgspec-0.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:10d0d1d464960d99a949f7ca01ef8928e51c472433a5f5ab74b2d695fb830652", size = 196779, upload-time = "2026-09-29T14:13:23.62Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f3/3c87372bac651b37911e0dc6926c3958949d3fcb8cec1016adbc44d948b2/msgspec-0.22.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e79725246291516a7359caad5fb743ddc0ec66ed40d2381fb846325b5031504e", size = 205214, upload-time = "2026-09-29T14:13:25.158Z" }, + { url = "https://files.pythonhosted.org/packages/43/4c/fbccd6e0fbbdf10c4d9b6bac8a26148dd5483b3ffff6d6c5a376ff1f5cb1/msgspec-0.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:38f7022fbe91954b31afe3888a0af1b652e0f370fafdeb1d425f4a814d789c9f", size = 196941, upload-time = "2026-09-29T14:13:26.637Z" }, + { url = "https://files.pythonhosted.org/packages/55/04/8db7186d3ae8818356bc623cc132db8b77da37ce4b1345f35719c8ad5726/msgspec-0.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6d3ca19a8ff28d0a67a1824e2bff7ec649ec795c80a265f20ade4caa63080de", size = 229934, upload-time = "2026-09-29T14:13:28.285Z" }, + { url = "https://files.pythonhosted.org/packages/17/24/a249f3491cabbe77cc65a1a6f87c128582aa39357227149be61cac8e554f/msgspec-0.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8b98ae215a102cbf6635f7df45f5c4af12f77fad1f7b71b9808fcf868a5735d", size = 234378, upload-time = "2026-09-29T14:13:29.821Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/6dbcb1b5de8e9d47e8f0fde9a288628dc178c1749a570b98251218fa10c4/msgspec-0.22.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0aa0cc3f18c35bab79bd7b87fde95d6274a9deddeebd1ea541f8066a5073165", size = 243118, upload-time = "2026-09-29T14:13:31.544Z" }, + { url = "https://files.pythonhosted.org/packages/79/03/7dd2d0ca988600e01fc00ad0cf20d1d44bc59369a913c988654c65f6582b/msgspec-0.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c8e84789918fbc15a503b92a829115ddd7567ecd3e4778bd418c56abbb86c11", size = 234557, upload-time = "2026-09-29T14:13:33.068Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/43f3c63bff1650efcaaea31466246e28b46927323fc9ff416c68cc6e4047/msgspec-0.22.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3ca7d4cd69fbb66bd2da6211d3e79d40542d196c16c6d99bf838f76767ad35be", size = 241288, upload-time = "2026-09-29T14:13:34.532Z" }, + { url = "https://files.pythonhosted.org/packages/8b/70/11b93815a59674f33182dc3e873d343ca0b37e25be52ecb28f52092f1fed/msgspec-0.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:28f53f3604dd3e70225f7563c831628dbb03299b428f8e62aadb4b628e386874", size = 236432, upload-time = "2026-09-29T14:13:36.083Z" }, + { url = "https://files.pythonhosted.org/packages/b7/82/7aad0f033f8dcb3f23868773c2ede803ae162a784828ccde75aa3f9b2f9d/msgspec-0.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7293dee54de040cfa225c22151cc3d72f17cd674b5ebcb52f38fb9f5701592e6", size = 202062, upload-time = "2026-09-29T14:13:37.955Z" }, + { url = "https://files.pythonhosted.org/packages/e3/45/cf52577926d73e2369e25927e389cb4ea1461169c489f46d3248159b5be7/msgspec-0.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c3c510aba9015c085e514b75a9b3f1ed7c4591ae5e379655821b8bba51f30cc7", size = 201686, upload-time = "2026-09-29T14:13:39.42Z" }, + { url = "https://files.pythonhosted.org/packages/c8/63/d93937e2aae34ff1ea33b62799d1963cacc1bf432d196d6130039657a122/msgspec-0.22.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:263e110955ed76fe0af2d79f819903b50a70dc0e7a752eb7aabe79d2e0a084fb", size = 202241, upload-time = "2026-09-29T14:13:40.919Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e2/46ece11a244cd56432eb2362ffbb8014f3f02963136d84d941f71fdc2a3f/msgspec-0.22.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c6f06576eced70462179a4b4638e84cf69fdbba37f44d13a64a21739c131a830", size = 194232, upload-time = "2026-09-29T14:13:42.454Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b1/1c385f2f93006cdc2af1511cc512c347cb22e2d4f11952c205230aedf586/msgspec-0.22.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d67582478b0eaabb899f2fb255c878ee7de57dff80eb73ab24f1865524ec441", size = 226524, upload-time = "2026-09-29T14:13:43.876Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fb/c80c8842d40347cacf89a60a4986b849dae1a6dfd25830441efdd6faa65b/msgspec-0.22.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:71cbbdb39631064e2f2f9e9ac2b1b69931d72276eb5f9da4ed025726296bdbb6", size = 231816, upload-time = "2026-09-29T14:13:45.329Z" }, + { url = "https://files.pythonhosted.org/packages/73/ac/90bbcfd890b4bda90c93f7e1b7fc24e84b270420486d9d43ae31443d15ab/msgspec-0.22.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0a5c25516e2034b2db7767081759ff8996e214def9c43b3055f61e1be1caad", size = 244241, upload-time = "2026-09-29T14:13:46.851Z" }, + { url = "https://files.pythonhosted.org/packages/72/9a/eabdb5f1b5e6013b0e2f9f2a95790587f6864aa9ca37f9d7dece65b53878/msgspec-0.22.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a1dab6a99c759d1391ab2993388c1892746a697254f4b5dc6c059ca6e3bfbc8b", size = 230198, upload-time = "2026-09-29T14:13:48.296Z" }, + { url = "https://files.pythonhosted.org/packages/e9/89/9f080532d4ac52f416dd7318e55c2053cc071853d17d58e24897a5b553bf/msgspec-0.22.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:a52eba5c9528fd181fcec39d22b67aaa1dccc6cfe8e24d3f5d41130e6d04289d", size = 242949, upload-time = "2026-09-29T14:13:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/6baf9b2f3523ebe2b820820c7929fd72ec5f483a93147130338ecc353fac/msgspec-0.22.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1e547966017265c0d23342bcf2e027305dde40ea042d16694a9b96b4f696a052", size = 233914, upload-time = "2026-09-29T14:13:51.5Z" }, + { url = "https://files.pythonhosted.org/packages/bb/37/9cf650779c8c1e53291ef184c838703930a4cabb1fb37e222c85a7d49fa9/msgspec-0.22.0-cp315-cp315-win_amd64.whl", hash = "sha256:0067057df265795f742658b15dbe53f3b6f21d19dcfa53676db11088cfa41e0a", size = 197910, upload-time = "2026-09-29T14:13:53.071Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ce/2f78c93d4f69e0167a19c2d40d4fbf7bbd6f074e1047536735832a4368ee/msgspec-0.22.0-cp315-cp315-win_arm64.whl", hash = "sha256:05dbc8268e50c9232ec72b9af1c7b13049aade4d1197764e38c427048706e046", size = 197590, upload-time = "2026-09-29T14:13:54.47Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bf/282e9a443058b85b8f706c9a651e2d8cdd11cc09d16e8fa347b6c57b75bb/msgspec-0.22.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:b3113ebcceeb7693a915183c73d92c10bf5c62851dd187cab43bd025fb587419", size = 206298, upload-time = "2026-09-29T14:13:55.913Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2d/2e694fa46f55319007f72013b17341ea3868be1c77e7a597176b202dda92/msgspec-0.22.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dfadea8bdcfafc614bd031de55a8ede22b43445cfff6d8b77cc0c07d3edc8a8", size = 198145, upload-time = "2026-09-29T14:13:57.412Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2e/2fa279cb57cb47175ae604d572787f903d4ad3f0afa867201bbd99e6647e/msgspec-0.22.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7a738826936c72348c613061d260446f13c82b6fd7d5d7705b6911ab8dca2f3", size = 232362, upload-time = "2026-09-29T14:13:58.817Z" }, + { url = "https://files.pythonhosted.org/packages/a0/58/a7e759b11b28441c27f803b29d9b5f4b5ad85150c89354b5ede1baca9258/msgspec-0.22.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2ddea9d78d09460f06c26a7a508adcd049761c3208776162b8eb79b8a032cff", size = 235885, upload-time = "2026-09-29T14:14:00.381Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/8d7ee098e94cbd9f35fa643dc497e06a4a6307b9f562cfbe48103fc3b209/msgspec-0.22.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884c28c80b0a511595b29a9b04a3a230c3797369e4a033e6d5c6d9b5427f8e09", size = 248155, upload-time = "2026-09-29T14:14:01.945Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6d/1cabb4b8a5dbf696e2b24df9e482b2e0333bb3b1b13ebb5433813e6616ec/msgspec-0.22.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f7a923bcde480065c8e25967464cfb2a687ee67000bb43157e2d57e40eca7305", size = 236416, upload-time = "2026-09-29T14:14:03.363Z" }, + { url = "https://files.pythonhosted.org/packages/ba/43/8bf0f558eb369f1f2d494b3d5ab9d0ae0907d07ecc0cdbe11b6768b02867/msgspec-0.22.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:65eea14bc65ccfeb8f3af62cb204841871e2961f002d7fa87dbe0f79dacf1c1c", size = 247292, upload-time = "2026-09-29T14:14:04.829Z" }, + { url = "https://files.pythonhosted.org/packages/81/33/2fbaadf98b5510cac4bb56d2b03937e0b1fb4bfcd1ae6aba20361f299583/msgspec-0.22.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:0666a1520cab86796612e794e71107e0fbf5e8ff3ddcdfcfff8f1d94b860d2f1", size = 238220, upload-time = "2026-09-29T14:14:06.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/b6be6041098ab859a8472983ccc2c08339fc2ef53f28d4f5fe7f4f34276b/msgspec-0.22.0-cp315-cp315t-win_amd64.whl", hash = "sha256:885c6e0c89d6103648525fe62aa78d600054dedf7b3713d23b15d7ddb6d66a13", size = 202939, upload-time = "2026-09-29T14:14:08.079Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c1/664578dd98be70cd4ab1a9dcf3a181b1376b83c65ec41ee162130b58c8c0/msgspec-0.22.0-cp315-cp315t-win_arm64.whl", hash = "sha256:268594d0bae5510572599a6ab0364dd9de43c867d24a30856cd9f5edb63d8dc6", size = 202117, upload-time = "2026-09-29T14:14:09.891Z" }, +] + +[[package]] +name = "narwhals" +version = "2.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/68/5351e34623d253423240ea7de3f8fc74fa8ab14b1ab3c0ec4ac8997413c9/narwhals-2.26.0.tar.gz", hash = "sha256:6b9cadca82f375c7e4cf584fdc86ca25da54827307a9c58f94547ee6104b82dd", size = 686970, upload-time = "2026-09-08T13:32:08.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b5/1b84b2c784db76d69442334bc8b8748c840f13ca53be086f4f250ad4a0bc/narwhals-2.26.0-py3-none-any.whl", hash = "sha256:29326d74f107c347fd1009bd58e38d9f7c7c5b51e6de97bc93dbc325d9038b54", size = 474034, upload-time = "2026-09-08T13:32:07.159Z" }, +] + +[[package]] +name = "nbformat" +version = "5.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/72/b3446efab8756e7df4b8ec587f8e611cb5a7249e4323db480802f1d3be04/nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d", size = 147775, upload-time = "2026-08-17T08:10:51.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/69/ee613f74085ca7103f79cd08d579c4f3177d1d26e5d3d9528d2d6536a707/nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec", size = 79849, upload-time = "2026-08-17T08:10:50.18Z" }, +] + [[package]] name = "packaging" version = "26.3" @@ -97,6 +519,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] +[[package]] +name = "parso" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/c8/721b3855fe457da514fe249247d404b9b39c5d16532278f70ebaa6acf18b/platformdirs-4.12.2.tar.gz", hash = "sha256:eab5f70271a490ef74618bb314fbb86e3c7e82fa3b9c922c2ea0e0a1a155d329", size = 59353, upload-time = "2026-09-29T18:27:56.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/89/446044f33aba0348d35e433f56d12206d010a5281a1df54054d4cfb82388/platformdirs-4.12.2-py3-none-any.whl", hash = "sha256:29dbf06d96c500bc6bdbce75fb0a14d63279c93b1842f97e72a135b33e856983", size = 32457, upload-time = "2026-09-29T18:27:54.43Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -106,6 +546,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pygments" version = "2.21.0" @@ -115,6 +592,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/17/2db4b414de89659144488e0d9c6c0bf0c8395841dc12d81d0532cc6ef310/pymdown_extensions-11.0.2.tar.gz", hash = "sha256:9506fcbe66fa355a775b768084334238dd6805020ac4b92bea0c0dda6f8f223d", size = 855419, upload-time = "2026-08-22T19:28:47.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/43/9f45ec4d14e596efc32c925a78104934790438b0c0628b70d741016734ad/pymdown_extensions-11.0.2-py3-none-any.whl", hash = "sha256:259910762019732caa1dfd76f3faa62c59f191d46573e80bcb1d13c0f675bbe5", size = 269929, upload-time = "2026-08-22T19:28:45.389Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -131,6 +621,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -186,6 +685,207 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyzmq" +version = "27.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316, upload-time = "2026-08-20T19:08:21.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/2e/8897afa4538707d86645f51cc50e66b2b84900edb1be9dc9af2c2fc04e5d/pyzmq-27.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9216132843d139a123f243c07fe70f7487dce5041093dd77040f9adb5dc91872", size = 1457109, upload-time = "2026-08-20T19:06:26.022Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bc/dbce7bc1654fa25b1e68b9bad9e547906f581ce919c186a88ed951cb794c/pyzmq-27.2.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d41ebb260b69329b7d4a2936d44c872c86dd785355b51366c8b14e07ed7e9373", size = 985701, upload-time = "2026-08-20T19:06:27.481Z" }, + { url = "https://files.pythonhosted.org/packages/95/cf/6981738b57c83fef33f356141ad83bf51e92f2f70c9d5767affd1a699f07/pyzmq-27.2.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468139ddb2e494d06e586bd3a6835077e8b3764560c8db552fe685c5867fc24e", size = 714285, upload-time = "2026-08-20T19:06:28.962Z" }, + { url = "https://files.pythonhosted.org/packages/50/b5/13657961a845e29c28a4e7ac4202999ec90b3bba1890a5469ce2ae90359d/pyzmq-27.2.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39755dc4a923021bd0677990ffdbc21cff0e1ee1cf07fe3817acea153ef4cb67", size = 888465, upload-time = "2026-08-20T19:06:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/58/5a/ca7ee7a767413d4ba858e93748b95e30b35b8c139849fba94de4433ea2e5/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:714f8cbd66c7e405338d668f79d2fe83fe923defe348e843be998603cf92eeff", size = 1705731, upload-time = "2026-08-20T19:06:31.819Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/083f6184e4eba566c9a3cc9974b1b0fe327b7093788135ba8133edaa67a6/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1132805970045adb9f5f05dd57040978286a8e21a5475f2c2ddf1bc983b9a2c7", size = 2074243, upload-time = "2026-08-20T19:06:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/57/f5/249362b664ae725d534c8843214fa9fd7fccd74532a19e24603954a88a7d/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b26f2d0493b79ce3c3112c8a12649418915582ba4707b8ed9f44febf2be71f42", size = 1926859, upload-time = "2026-08-20T19:06:34.796Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/23c613c15f06d879f13364d14c17e5e4e8304049411e96c1410e6e56c3ea/pyzmq-27.2.0-cp311-cp311-win32.whl", hash = "sha256:44f261eca7dfb9904ea2b56428f59ab693bbe2715c0413a701f17b067ebf877c", size = 570747, upload-time = "2026-08-20T19:06:36.337Z" }, + { url = "https://files.pythonhosted.org/packages/dc/bc/bbbcf89003c93f18e33665c26e3c48d75e3915c3dd22887f3a7aea2c5e26/pyzmq-27.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b86e04f55af0f4d8cd8ecf14c0b8b81ebc8fd66fa20126b753514628ecadc7e", size = 644845, upload-time = "2026-08-20T19:06:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/14/c5/4635d0ba2b8493edf6d5541fff0b07fa1d986fdfc29c596a53a21e20f9af/pyzmq-27.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:917d601e9540098f580d2723d0ce6402cdb6f02bc8dc2de74e0dca6e13bffd1b", size = 567495, upload-time = "2026-08-20T19:06:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/153532fa53db30e116118164f3af269a1f3966b3e2ba32c89b12fe864bd8/pyzmq-27.2.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241", size = 1431074, upload-time = "2026-08-20T19:06:40.601Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ef/c08b91248bb90a9efa81fa00ba81b69c157c74d0c5efbb2c319d91babb62/pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef", size = 973915, upload-time = "2026-08-20T19:06:42.037Z" }, + { url = "https://files.pythonhosted.org/packages/b4/78/a3a3a86c2b00fadb92ece1ca4f8f028d62b2ce9ac3526097239ab2d6fba9/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109", size = 697722, upload-time = "2026-08-20T19:06:43.325Z" }, + { url = "https://files.pythonhosted.org/packages/62/2c/d5828306f795e8d34676d266823b74e2101e0ad3760d12083de3e02abbb2/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43", size = 872258, upload-time = "2026-08-20T19:06:44.627Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/51253b78fd8739293e283407eeecb14215c02c71b6519af21f6eed8e69cd/pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5", size = 739591, upload-time = "2026-08-20T19:06:46.214Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3e/142c85b67a4c9678629b0cf6d5125b29663d75be69bfaa57a3cac344d780/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394", size = 1689031, upload-time = "2026-08-20T19:06:47.612Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ee/0776fb0f98ed1eb74d77240087fef0ab045b6ad15cb09555c6c5134c98ad/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3", size = 2059547, upload-time = "2026-08-20T19:06:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/ec77f691a4aebe29ab6329f996fb0e0270c876a3016086e3ca6ef733bcae/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42", size = 1910457, upload-time = "2026-08-20T19:06:50.783Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/1f5530ff4fc271b4597048371d5af972c2baab51be132ba15874e0327a6a/pyzmq-27.2.0-cp312-abi3-win32.whl", hash = "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2", size = 563450, upload-time = "2026-08-20T19:06:52.307Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12", size = 628633, upload-time = "2026-08-20T19:06:53.809Z" }, + { url = "https://files.pythonhosted.org/packages/52/aa/3918b5ac7f9987bd9c421b065074fd7409ded88f856f2c704a24341877ec/pyzmq-27.2.0-cp312-abi3-win_arm64.whl", hash = "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3", size = 556006, upload-time = "2026-08-20T19:06:55.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/d0541596b48c5a19f85dcbea83d6673d8e91681cdf853eb194c31fc9766e/pyzmq-27.2.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52", size = 1127193, upload-time = "2026-08-20T19:06:56.545Z" }, + { url = "https://files.pythonhosted.org/packages/50/9f/8c7411bb283982d46e6d56dca6a095678c87eb0398daead12776d9881ac2/pyzmq-27.2.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb", size = 1166833, upload-time = "2026-08-20T19:06:58.011Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/a849161ff88b2de9b991cc8ab332218824741122fdc4fdf222a5b822ac8c/pyzmq-27.2.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702", size = 1134452, upload-time = "2026-08-20T19:06:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/3c/34/ff4aaff0cfba2a4d7ad1a16ffedc52c6deb89fcf673d455085446b23f215/pyzmq-27.2.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8", size = 1167520, upload-time = "2026-08-20T19:07:01.283Z" }, + { url = "https://files.pythonhosted.org/packages/b6/07/42111e9dc1041d78b4443d6eb1b82b027f1a58178dc8a38385effbc72ad5/pyzmq-27.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7", size = 1466289, upload-time = "2026-08-20T19:07:02.738Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b4/def7a478458da78665840564161772e7e938600c32a89f28e8b221b54d2d/pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f", size = 975868, upload-time = "2026-08-20T19:07:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/38/d5/e3e85f7fea37153097aaff49db9e33093909cc2a7b22c1ac4ebe546600fc/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1", size = 706054, upload-time = "2026-08-20T19:07:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/3b7d9449b223183222bf517245e1e53d5f1ab8c10be8b45f6a301b2f994a/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853", size = 878984, upload-time = "2026-08-20T19:07:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/be/a5/8b49dbd494f6dcfda69dc4cade322a4b02706ef4e3d30cc366d4e369899f/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd", size = 1697489, upload-time = "2026-08-20T19:07:08.945Z" }, + { url = "https://files.pythonhosted.org/packages/da/5a/4bb8280901130c26ea25f0cbb4a6d39d94250860c6b3dbd912f1cf48fca7/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af", size = 2064236, upload-time = "2026-08-20T19:07:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/f433af66922554adb2b5f79e897018c8e19a90b9eaeb49c4814f8355ebe4/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103", size = 1917424, upload-time = "2026-08-20T19:07:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/36/81/ea1c1ae3f801d96ba2c269e056761ebcfe023476e651d3af2a7817962051/pyzmq-27.2.0-cp314-cp314t-win32.whl", hash = "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843", size = 591103, upload-time = "2026-08-20T19:07:13.451Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/149a627707e780fa9f2c1ede3590c14fa6b18b5576d15744342622299a50/pyzmq-27.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a", size = 670215, upload-time = "2026-08-20T19:07:15.069Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/f9c3c1536c41ef3dbf765ea04218990e2056e558f98184ecd883767fc501/pyzmq-27.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101", size = 582252, upload-time = "2026-08-20T19:07:16.582Z" }, + { url = "https://files.pythonhosted.org/packages/fa/00/78fe097a304a408275747ce43f20428789130b059c5649956277c20f30cf/pyzmq-27.2.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b", size = 1134295, upload-time = "2026-08-20T19:07:17.94Z" }, + { url = "https://files.pythonhosted.org/packages/f2/83/1c36270658d2ee56e23a3f9ef5fbcb94cbd2f9fe966a6641f2f38e697162/pyzmq-27.2.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9", size = 1167492, upload-time = "2026-08-20T19:07:19.476Z" }, + { url = "https://files.pythonhosted.org/packages/58/b2/f0ae223438d7faa991f6feefdc823815f11cc604f898738376b59fd96515/pyzmq-27.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f", size = 1465992, upload-time = "2026-08-20T19:07:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/59/46/fb56f3f37a6a0937b0e1d2885e808b5eedc171320bac85573cfae78fa9bc/pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6", size = 976118, upload-time = "2026-08-20T19:07:22.577Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/a2c9bfd7c4d34eea1278493cd041bc000d41acb4463c89ceaad29dc813b6/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44", size = 705968, upload-time = "2026-08-20T19:07:24.019Z" }, + { url = "https://files.pythonhosted.org/packages/d6/12/b906b269116b6591dc15c0acc5d04c043957c8a531d336999731f4b1d899/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b", size = 879011, upload-time = "2026-08-20T19:07:25.428Z" }, + { url = "https://files.pythonhosted.org/packages/12/13/f96359534bfb77651c15f1fbfc4bfdd7ec3489d23f434706d39598dd0dcd/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d", size = 1697496, upload-time = "2026-08-20T19:07:26.821Z" }, + { url = "https://files.pythonhosted.org/packages/21/b4/2c007ae5f2fe5eca86cbfbc874ed86b5135f2f7812615dfd78606d3c93f6/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b", size = 2064347, upload-time = "2026-08-20T19:07:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/9b/88/767af3a6630c15215f3a66700ec79598a375edd1fdc9d75a3ad522178c01/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012", size = 1917360, upload-time = "2026-08-20T19:07:29.693Z" }, + { url = "https://files.pythonhosted.org/packages/35/c1/80dd2d20d6e57bc68e1dce1e84bf3e76c9577c1bf728199985c8b4ea0fd1/pyzmq-27.2.0-cp315-cp315t-win32.whl", hash = "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb", size = 591073, upload-time = "2026-08-20T19:07:31.341Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b5/33b781666f3f52ae834bc9c8e38f4f0483a826c5a91cccc993292007bf10/pyzmq-27.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35", size = 670701, upload-time = "2026-08-20T19:07:32.895Z" }, + { url = "https://files.pythonhosted.org/packages/6e/97/bc4f0edefb992df4fdebcf9f0cc40f631cd4ed277e1ed59ef2cd99a5c8c5/pyzmq-27.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d", size = 581985, upload-time = "2026-08-20T19:07:34.19Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/7187a1f0bf2b8bf8dc6b91762438fb9b472f684f2dc4cb74a24bf8957943/pyzmq-27.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a7c1144dc61777938e932a2c9011b980b89fd8ff3733033b34c44c299187a6e1", size = 875015, upload-time = "2026-08-20T19:08:01.692Z" }, + { url = "https://files.pythonhosted.org/packages/92/71/09b71620ad52bad4eb68b1516978ecaf52ef623c3fa16e0732a03cf3274c/pyzmq-27.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c218b816220d05acf6ab1bafca58926d95cbcc5fec5024724666030466308f0c", size = 774395, upload-time = "2026-08-20T19:08:03.108Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/5c8eb9994a14ff5ee5b0cada339421748746c95aee0280c8b656741e8749/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ae6ebbc0bfe5a21ce21e32ba567bf73df2d93888109c65acbd42506cf9395759", size = 877994, upload-time = "2026-08-20T19:08:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/97/64/e22094c5555e550b6450ecfcceb6a1205d893d9a18ae27764c8c45acfb16/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:679b5b1dde326a921ea2c9ec1f9ea3115bfe1b4735779bbc6eb0473a0ed93f71", size = 614492, upload-time = "2026-08-20T19:08:06.487Z" }, + { url = "https://files.pythonhosted.org/packages/d2/28/5b1042899caed18278c56d54a502f5254d463afe8aea1acbecc98e053391/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5c6d8744d10b5e1eadd90a7c58f8546acf6bf680ee463f7e6ada09ad6c9f802", size = 780574, upload-time = "2026-08-20T19:08:07.987Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/f134603a671c114c6b56eb912bba890f09e2d43b8a28d243be5a5507cf2f/pyzmq-27.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3ee8dd7031d5e23f632e0e7eee67183ca7d2536e0de35dc1e5d69f3471a791e8", size = 553961, upload-time = "2026-08-20T19:08:09.651Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + [[package]] name = "ruff" version = "0.16.10" @@ -211,6 +911,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, ] +[[package]] +name = "starlette" +version = "1.7.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/7b/2b/3850dc6bf7ef71b088962eba31dafc6cffd2f96e577ebb0bb316df96da3e/starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d", size = 2736246, upload-time = "2026-09-23T07:30:26.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e", size = 78980, upload-time = "2026-09-23T07:30:24.567Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/96/e07752635b98536177fa1f37671c8f3cdde2e724c6bcf6034b2cfb571565/tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97", size = 180129, upload-time = "2026-07-17T01:48:04.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/bc/8c13eb66537dce1d2bd3a57132902f38d0e7f5bb46fa9f4daed9fe9d76ee/tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304", size = 49449, upload-time = "2026-07-17T01:48:05.728Z" }, +] + +[[package]] +name = "traitlets" +version = "5.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/2e/a7fbfe268c8a3b32546930c0297c101d65a4a14c304ad5790a9f478f0e4e/traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1", size = 166137, upload-time = "2026-08-03T08:32:36.848Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/66/0d785f0bc5e4315a96c989bb476d0fc07ea4f85132550c7b156ca2035d52/traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b", size = 86211, upload-time = "2026-08-03T08:32:34.48Z" }, +] + [[package]] name = "typedstandards" source = { editable = "." } @@ -221,6 +952,8 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "marimo" }, + { name = "nbformat" }, { name = "pytest" }, { name = "ruff" }, ] @@ -233,6 +966,8 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "marimo", specifier = ">=0.25.1" }, + { name = "nbformat", specifier = ">=5.11.1" }, { name = "pytest", specifier = ">=8.4" }, { name = "ruff", specifier = ">=0.14" }, ] @@ -245,3 +980,171 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3 wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] + +[[package]] +name = "uvicorn" +version = "0.54.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" }, +] + +[[package]] +name = "websockets" +version = "17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/89/3f825ab71c242fffb62ea8fe638741c290f62f8d7aadf8125ff897747af3/websockets-17.2.tar.gz", hash = "sha256:36c2fb94c990cc2545143b12690e2de6c16300f9dbe5b4f33fa300cf57dc8792", size = 188355, upload-time = "2026-10-03T14:56:53.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/f7/8a90cc2abbe4709dff4450824beb07cbf7256566ee043c2ba3faa1d5fb2a/websockets-17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:569ed5db651e420b13279f9333443bb5b84a436cc66b599cbc535697ae4434a0", size = 217725, upload-time = "2026-10-03T14:52:50.797Z" }, + { url = "https://files.pythonhosted.org/packages/7f/85/e418ba2e7e412a5b35c42caf6d4fcc8ecee1a66edc4f2a5f780da775aa77/websockets-17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3892d76754b5f36fb40619f3ef09c68e5c3091f1ab8840964518ae5a41f30952", size = 215415, upload-time = "2026-10-03T14:52:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/e4d7eb2e2e4ffed0b0dfbd2d1aa3c8101f42d34ac9f58b47b822c565d1d4/websockets-17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5436ffea003adb50e283ca0684a3fcaa1396104f841736c3322ee6582bd09e98", size = 215690, upload-time = "2026-10-03T14:52:54.173Z" }, + { url = "https://files.pythonhosted.org/packages/4b/dd/e8718fa6114c4cd15b05133b548af985638e80774253c1faee8d49874c38/websockets-17.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9df9d048def11365d170b375b6ffc8b23a7f188c3560acd4418ba088ca2e2705", size = 224756, upload-time = "2026-10-03T14:52:56.132Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d5161c46f3eee2ae67cdec489532b51695a1c27ccfadd858dcd419ea26ac/websockets-17.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:376a693697ddb695ea282ead76060f4847f90e564b12b4389f2c7589e6fadb9e", size = 225026, upload-time = "2026-10-03T14:52:57.671Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/3f83bace9636af07d7bb00cbae0bcb5bd1697892babac79664f3a2b3a011/websockets-17.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd63d0c7ed0d3d719c91b5a3861f0f0b3cec9bf223033ddf69d17aaac74bb6d", size = 226260, upload-time = "2026-10-03T14:52:59.114Z" }, + { url = "https://files.pythonhosted.org/packages/03/50/5347cb13f97430526b9c31e9b30fa639bb1d0f9d53074da8622b327cfb6f/websockets-17.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:48997ed4431d8006988788ef4b62e1fd3f053c7463b4fa793aa6c4f9e96a3bb7", size = 229573, upload-time = "2026-10-03T14:53:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/14/2b/7511082e3fe0cc3233ecb0c3b019ef12c1cd9df60ac1a7858f6093f490b5/websockets-17.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e312e07557a5ad348f4e83d3419773527f6e790c7f97928b1911d767b6ea1c7", size = 226819, upload-time = "2026-10-03T14:53:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/4f/86c1a9db323d4fdbf56cc089942f18328a48c3efbbad0d625a66a2195842/websockets-17.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:902ce8cafca2dc14cef9558a6fc3b45dbf7f121d1404bf2ad18a1c894555e48c", size = 225595, upload-time = "2026-10-03T14:53:03.768Z" }, + { url = "https://files.pythonhosted.org/packages/81/92/4f54f6031d97e284e01a0728cef38b095478dcaab81837aac8cb0e26ea6a/websockets-17.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e53d950e16d4bb672a5ff41fe3131e65a4e5d688d694e1c7074c8c9990bb3ceb", size = 222875, upload-time = "2026-10-03T14:53:05.7Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/c6d59b8b45c730a56ee5acf6c0ce9896356cba25ef3f9a4c9d1796f2e44f/websockets-17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:946ac2164d646e733004946ae39536b5af473853183d81da5962e29d36e3ad35", size = 225745, upload-time = "2026-10-03T14:53:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/5d9b91b43aa339b96551630940a847270c10a9d70243be4c81fe5dc6fb34/websockets-17.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:660aa158127035e741d4b1835dbe79ae18a1fbb21ecd236655f31d60110e68d5", size = 224342, upload-time = "2026-10-03T14:53:08.893Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e1/c90c24b0dfb12b8b6f0d5e13fc7cf9f121a2e072f7f54bb888da826b2012/websockets-17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4733fc2d99fe888261417b7e29995403a72d9ffa78629902882325ea141177f2", size = 225109, upload-time = "2026-10-03T14:53:10.495Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5b/f38ca1299c10ea1cfc7f1d129c65a15e4f4b281d1f3dc25891d5fb9bf9db/websockets-17.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c2ec7e51157a3fa0e9cfdb1a8969bab38d1c22ad1ace7c6cea006383b43a1ad4", size = 226151, upload-time = "2026-10-03T14:53:11.976Z" }, + { url = "https://files.pythonhosted.org/packages/f9/21/ff6089c6921c7ae0e1801a4948aa1a3831deb1596e8f0d1cd3a0c0e44109/websockets-17.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ada04d0262ab06527054a2a497f384d102698ff39b3865dc566a7d24b6f4058c", size = 223732, upload-time = "2026-10-03T14:53:13.864Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c4/01ca4212f665e351123c84e7f7156badf5da958ef8aad8781b538682c699/websockets-17.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c393a202df08e96ed619310f0cd78be700e532a57d9a6ceee5f80b4e35bef14", size = 224764, upload-time = "2026-10-03T14:53:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/bc17b39d1e62b771d8a417b714439252d7abfca21185242cc293d75b20d5/websockets-17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:af4c565b923bb5975401b8e4cedc2e17b2fdbf33b905737ee12384e6a6fd9507", size = 225002, upload-time = "2026-10-03T14:53:16.93Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f6/ccab831ab6a841a35134937a1794c0f3f09ccc604625505be061dec5b3e4/websockets-17.2-cp311-cp311-win32.whl", hash = "sha256:c81d6cdbacccda7e0eef3b076a457fd14c3835cdbc5993d2881580c2fb1f5f26", size = 218226, upload-time = "2026-10-03T14:53:18.376Z" }, + { url = "https://files.pythonhosted.org/packages/0a/18/4fcc23f2159393ad7a668574ee97ee5a135003bfcbdd56b30581110c0fe8/websockets-17.2-cp311-cp311-win_amd64.whl", hash = "sha256:55c5b9eab079540bfb639b40b07b7b467e5c5a7ecf97a65cc8665781381c9856", size = 218523, upload-time = "2026-10-03T14:53:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/5a3f4f75dadb7fbf980ea4b59d02528f87fb2d3c0ac120c2ff50d1dc1b34/websockets-17.2-cp311-cp311-win_arm64.whl", hash = "sha256:55f9a808a0e072473337c240c939849818276e288e2374b832255b5b791b0851", size = 218454, upload-time = "2026-10-03T14:53:21.417Z" }, + { url = "https://files.pythonhosted.org/packages/bc/de/87854af9b38fe4738fd85f7f21c5b49558ae20aec898880894e435f33375/websockets-17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:916ebdfd82e7fc68041d36b2b5f60361b9abce1e087454da15f8bd004839e090", size = 217757, upload-time = "2026-10-03T14:53:23.029Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2e/1e80b5efa41544f626d56bd15ccb53dbfc56bf28bf80ab9cd6f82c4b1d20/websockets-17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3621f3686397708b8eeabfd0a9d75267c1f29a7537d2fe31e65d099e71587fa4", size = 215439, upload-time = "2026-10-03T14:53:24.531Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6e/82c78b595aee05be76a7ee78539323da1593c1848e4fef51c704c696568f/websockets-17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a81e19710d48da88653473b6b9c366d47e99fe4f58e37ce415be47966748f31f", size = 215703, upload-time = "2026-10-03T14:53:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c4/905ef6aa80423c03dba99e1e26fc0acf63a2a9a6a2d9e8c0e6a63caaf952/websockets-17.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f2731f9067976c8c4127212c0d2f2ada42d497d935e470419e029802365b12bb", size = 225023, upload-time = "2026-10-03T14:53:27.744Z" }, + { url = "https://files.pythonhosted.org/packages/03/c0/a6d8be9c43e4456fb9597fdf8b5e0ce1f0a5df41503acce6d869536e4e23/websockets-17.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6627b913b8586b1c06db9516b31dd0dfbc621de3bb9312616d92a7e44f268a5b", size = 225299, upload-time = "2026-10-03T14:53:29.171Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/976d34b5491258b0a86c2ce9b9aabb9fdd68919ffd7fe65999c14a502a98/websockets-17.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0198c4ec6a3406a2f7557c032967de426474c2c995c81076585e09d29a9f407b", size = 226540, upload-time = "2026-10-03T14:53:31.635Z" }, + { url = "https://files.pythonhosted.org/packages/83/2f/c4cfd42f53c697a8ed123fd82b8f85fcd13b6360d47f9f1d1d45d6ec6627/websockets-17.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:88c6a42c2632ff469e84155e44f6ed92cb15ccb047bf5fcb59225ae5a12fd33d", size = 229371, upload-time = "2026-10-03T14:53:33.061Z" }, + { url = "https://files.pythonhosted.org/packages/e7/55/9a221b29c6232ff9282eecb2fc102402cb9e42a3479264db0e5fc4fe6835/websockets-17.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:eb0023e6cdb4b8ece0b33875188dd16104ad8c335361d396a98394f99e30ff7a", size = 227173, upload-time = "2026-10-03T14:53:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/8f/07/125e6d010c56c253d3d2b93cabaea0f96d33898151a16b49066a594acecf/websockets-17.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c1c09d5d4646eb96bda2cfb97493bcea21a0956a981de116e6b1f4a9de07f3fd", size = 225929, upload-time = "2026-10-03T14:53:36.071Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/aad3bd902aee84e1b261ad6ab83b405e4a564af43101b8ad1dc0293ff4f4/websockets-17.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0360c4dc13ac569cc245e0efa2f4d4b1e4733d24c47b8ab3f3747227b1356348", size = 223167, upload-time = "2026-10-03T14:53:37.528Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f4/ec8ab9be1a5310b4fea829f088c7aa2b7a58b61d34bce1b2a9338635ff12/websockets-17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76693a16dead737946b651375ee3109d7db7ad9569a1c55c60aaed3ef85cfcc6", size = 225974, upload-time = "2026-10-03T14:53:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/ba6503f8257d3f98b0f07ebaad0fd099c9023eae744fd5b775416743597e/websockets-17.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:77a42cc507993ec5471b5283f7eef869239173b6000031543e3938a86d1af0fd", size = 224581, upload-time = "2026-10-03T14:53:40.496Z" }, + { url = "https://files.pythonhosted.org/packages/d0/45/05cca59a876c6776727d96fc7ba59e0b6f9aa496afbf13e7e04ad0b63678/websockets-17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3bbc5543e39ee025d524077c5c15c2d67bc11c9f6676afe5b531839e24d701f6", size = 225347, upload-time = "2026-10-03T14:53:42.061Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/cf0e43292ae949b13f67535be84317102891d69fd1986ec2bf2ead42747b/websockets-17.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8da58558bfb0ca6ccac2419773521f1111e40654038b1afabdfc69c02cb82614", size = 226457, upload-time = "2026-10-03T14:53:43.575Z" }, + { url = "https://files.pythonhosted.org/packages/79/0d/9a5c61a18f0cc9876d94c70ccb3daf7614a9fee56abbb37c0e64e757fb96/websockets-17.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:01420cb1cb47433e8e7075d32cb8017ad3ffed0654bd1e48c0251b865920dec3", size = 224011, upload-time = "2026-10-03T14:53:45.077Z" }, + { url = "https://files.pythonhosted.org/packages/34/ed/991c1ab80ab2ce40e1c939fef6fa8f971c3ef3b21caf988a7a107e0ad27d/websockets-17.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c49c9edd47d0e44d360299e2d8865e2950d2fcf1b4098782c9d7dcd070919e5a", size = 224990, upload-time = "2026-10-03T14:53:46.8Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7a/363c835d17923e967fb66376188e67b9a261c85d826a0cd5e4dd3471221d/websockets-17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96f6c8d0fe21930d1f982bfce2382789d2e8d005d2ab63d21280660f95ef8fe1", size = 225265, upload-time = "2026-10-03T14:53:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/c8/90/6c51f6d78636bd1cd6781fae8ea5ea7bf1d5b4059354f3c1f5f8de793338/websockets-17.2-cp312-cp312-win32.whl", hash = "sha256:b25659ab2d655d742701487d5591e3f98e8f8b329fc999e05e3d59691ab344a1", size = 218228, upload-time = "2026-10-03T14:53:49.867Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/90008411c652dcfae34345a2169f4becd066a4ba71eebfa8dd801e0445e1/websockets-17.2-cp312-cp312-win_amd64.whl", hash = "sha256:faa763b677e96f1beccc6b4d7e8c079dfeed2f249f57a19debc321b519ee64ec", size = 218528, upload-time = "2026-10-03T14:53:51.486Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a1/b8ad6c17f8e75ba2215422fffe0d7f0c4b690dcff1c47c0473db0d253d51/websockets-17.2-cp312-cp312-win_arm64.whl", hash = "sha256:63499fc49efe48bccc2fca40723bc7adb198866cbe159093dd979905316994b6", size = 218457, upload-time = "2026-10-03T14:53:52.938Z" }, + { url = "https://files.pythonhosted.org/packages/54/54/a935a32dbc2e7365b1b59eb74b5ab7515456f02370fdca4c4efc3574e96f/websockets-17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b24b83fbb34b2d8de06cf0f0d4bd7737344ef854482a614826d4356c0c3f0c12", size = 217752, upload-time = "2026-10-03T14:53:54.59Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/cb8881851abe2662730e6c61cc521b4c96513fdf9103a44f169afce2eba8/websockets-17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8a829db795e3f87053904493d184b185c8eb1f497c852f434168ec856aa6f997", size = 215436, upload-time = "2026-10-03T14:53:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1e/621bb93f35ab7d337be98f1958294437527e2a1797089b5e734ddc5eec5f/websockets-17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf8811d285acc91216368df7fb55cc8c9bf6fcd90eea42429c7186c7385a12b9", size = 215690, upload-time = "2026-10-03T14:53:57.587Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/49d0c983c082676d5d413b28e6ba5ae1d174c00268467bf78d9fe986a2d2/websockets-17.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89c4898da776193577279173dcf9860487590611d7320d379435a145881b048d", size = 225080, upload-time = "2026-10-03T14:53:59.081Z" }, + { url = "https://files.pythonhosted.org/packages/04/13/95a45eb410019772002d8f53d81396dad4120f7df39ca9962f86f5d7cd01/websockets-17.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d87091c4347daadbcc0833b65812ff38d7350c67339625d4e4a512cf38e3e8ef", size = 225361, upload-time = "2026-10-03T14:54:00.61Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/0f0eda80bb441f54becdaf793eb20ee080926f8d2356388377cf262187e5/websockets-17.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1110fbfd530c447380e6e6db88b7e43ffe33d54178f5b0ff0aaa5a280301e668", size = 226602, upload-time = "2026-10-03T14:54:02.098Z" }, + { url = "https://files.pythonhosted.org/packages/5c/36/067fc09d8e6f154abde7c2f747c52cc442a02c5eb14816f5c39cb9f8bcc6/websockets-17.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:83abd8beab056aa77a116364811f8fc262dffbcc7abea48de0c85ccbfc6f1428", size = 228035, upload-time = "2026-10-03T14:54:03.545Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a2/939bade7a396b4c381aebbf3941969f124d0f98d56753f81cd256f3fc4d6/websockets-17.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:876da8ca5520d65b5d0f2ca6b4e7a00d35bb90ccda35cb2ce3cda4b6c711e84a", size = 227227, upload-time = "2026-10-03T14:54:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8a/37b1033e21709dd7fa39239ea4d9cd7f348ad5bcba94eb47253878576f8a/websockets-17.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8462395df8f224d2daa3d80db3ae4450d9d4b7243c8483ac79a82862f1599dd6", size = 225985, upload-time = "2026-10-03T14:54:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3a/0d89539900b06d86366facb7558198046de125ab8c371d9248d6262da70d/websockets-17.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6e9a04e69456015e6ae5e0d486d995137fd435794442122b00ce5f9526ea3ba8", size = 223226, upload-time = "2026-10-03T14:54:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/31/9a/bfc5633e3d538d0a71cfbe7a5fee56c712e16c2dbd0ce17c83196a2a96a9/websockets-17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a2321bcb73758c44c8076509024d02c15ee484fe77ce04edea4bf4d257492cc", size = 226042, upload-time = "2026-10-03T14:54:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/cbaf1786d8e3aeafe9d76951fc01139ec353b92555580336f23669382a55/websockets-17.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8be4a87b3baca380ec3c7b1643b2dd268ac9d42c5097c0e8dc9a49342faf4774", size = 224639, upload-time = "2026-10-03T14:54:11.911Z" }, + { url = "https://files.pythonhosted.org/packages/80/49/175faa5bd169486f835602ac0ae6303318aa65693b79cdc72c5ee53b148d/websockets-17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:eb7b737ce8d18c8a08beb68f751572b7bf6a18093ecd1406ca1256b50592552e", size = 225407, upload-time = "2026-10-03T14:54:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d1/3662f612456cfb2dcc128c8e596f0a55fb7b695025e2ebe8ba2abb355c3b/websockets-17.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d6605630c2808b33f362d6d08582e79821f77ed2bd3f49f9d467ea70defea06d", size = 226513, upload-time = "2026-10-03T14:54:15.046Z" }, + { url = "https://files.pythonhosted.org/packages/73/6b/07af5177a49e30156b0922556fa93624a920a2b17d3e63bf4ad94668112c/websockets-17.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd9252828073fd0d69e7667af4275a1b17c18d0833b1ab7f59db272f194a6b9a", size = 224072, upload-time = "2026-10-03T14:54:16.574Z" }, + { url = "https://files.pythonhosted.org/packages/eb/34/d18054ff4d8314524164f8b8efec2cb17627287e099f122c28ed6fa598e0/websockets-17.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:06c7386128a9d85de4e1960114604f3031c084d2f4eee8db382637f1634cbab1", size = 225022, upload-time = "2026-10-03T14:54:18.143Z" }, + { url = "https://files.pythonhosted.org/packages/e9/12/75433caa3e9fa3e51d7751dc6bad24a86addf76cbfb51e52b11d037ba7fd/websockets-17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:98f2d03df74977fd252831c997c388cd6c3f691a8a9d022b266d3cbd9849838f", size = 225303, upload-time = "2026-10-03T14:54:19.679Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/23e21c002aa2786ac9807c0876faa3b2576493b29ca3386287b0db46f021/websockets-17.2-cp313-cp313-win32.whl", hash = "sha256:5b43a1f7e4853ce08c3f6d3bf69799ee5b46548bfb71792a8158f7e45d66b547", size = 218219, upload-time = "2026-10-03T14:54:21.232Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/960411c0c574535d629c16e96a2b4e5353dbe4109df8ecea859e1b5245ee/websockets-17.2-cp313-cp313-win_amd64.whl", hash = "sha256:27c7a59b5352a8f741b422820adfe89dfe47c8f2d84fb32111e76111edaa0e83", size = 218531, upload-time = "2026-10-03T14:54:23.025Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1a/3ac07bb52378952eff1d52d04a7ee6e82ce84e3da319a52a4739cd9c78f5/websockets-17.2-cp313-cp313-win_arm64.whl", hash = "sha256:533b7c82bb1eafbeb921dfe131c9f88e55451ddc328d84bde1c9340ba72d2808", size = 218466, upload-time = "2026-10-03T14:54:24.857Z" }, + { url = "https://files.pythonhosted.org/packages/8b/74/6bc991a28ac983600e65de408ebd1b1413d554ed0468ae5c831bc52dded6/websockets-17.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ecb748910e9ba4624ebe2057791df51dcbffb48c37108ab94a3c593472023c9e", size = 217791, upload-time = "2026-10-03T14:54:26.381Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2f/158e99426be6e71d09520bae53f29294fbb614b2fc5fbf8867b1d08395a7/websockets-17.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2ab9af5cb7265899e659f079eb71691375a1025b6d5fbd3caa495dd08f70833a", size = 215486, upload-time = "2026-10-03T14:54:27.962Z" }, + { url = "https://files.pythonhosted.org/packages/5c/09/1abf942723c0001d9c2fca1551907dade6304517b982b0bf10bba107fa81/websockets-17.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:06e46da092bca3a52e98f0458c66b247993ce501a07cd09c858be3296511ab7d", size = 215699, upload-time = "2026-10-03T14:54:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/1ade03963ef497c47e6bad79e24370827b2fe6145fa8f58070ff2b7dcbac/websockets-17.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fcce735ffd72ac4056db05325d9f0232382b74826f0196eb6a15ca903abdaa0f", size = 225081, upload-time = "2026-10-03T14:54:31.278Z" }, + { url = "https://files.pythonhosted.org/packages/9f/fd/47b8a0361c49da939b976a07b27a72a9f893d01dfcf4d2a28b53419ce1ef/websockets-17.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:42cbca10f82a8b2fb1536e8a0830ca6ceeb6bb3d8d64b766e0795369135654a8", size = 225430, upload-time = "2026-10-03T14:54:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/f0/26/f4d4c76264ee037c5556ab5f50fcba302746dabf7528955534e4dda9965e/websockets-17.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63ff5a21f26bd0e6a8464b53fadbe174825c8718ac14180df45665eaacdb6af", size = 226676, upload-time = "2026-10-03T14:54:34.833Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/c8b1c981322a050c4babfd327ffc9880f9c3834f5b15d2574e37eeb8768c/websockets-17.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:63f543463601c1558b755f8dd7618b6ec3dd0934dda051d3b7030d8c76e54de2", size = 228048, upload-time = "2026-10-03T14:54:36.424Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/1cb29ddb23e6bc27ffd1c5316cd3616360d1ba0c3854eaa134ee3207bd28/websockets-17.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4c32eb565ad9ce8a6444248e5b7a19dbb86a81c811fe5fcc2fba7a735aed5163", size = 227281, upload-time = "2026-10-03T14:54:38.01Z" }, + { url = "https://files.pythonhosted.org/packages/ba/64/135274572dc0c845fc1111e2b932c807c395daac75d6eae6cfa148d8a208/websockets-17.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d459bbb6c22f26dcebea56924a362aba50d453b9867912862c970434fcf0d94", size = 226025, upload-time = "2026-10-03T14:54:39.613Z" }, + { url = "https://files.pythonhosted.org/packages/58/75/f1e386aec3124489411caf5138cdd5a2bc43d3fd4a681c69adcf5f6272a5/websockets-17.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f19ca1a21871f024e38faf4107b433047df27558dff1b72a1dac31481e2c1fe5", size = 223277, upload-time = "2026-10-03T14:54:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/60/eb/24733a0f568c2eb99e60f9faa620a98fb228c06a01e7e2f348b33290ed9c/websockets-17.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c76b4bcbf0f713194591673fc86a42820e14da6bbd1bb445d3d002cc4d1e4521", size = 226148, upload-time = "2026-10-03T14:54:42.779Z" }, + { url = "https://files.pythonhosted.org/packages/55/6d/ea66a30af74f5983cae31ebb9ef78b178b366a12856a414e1472225c4a34/websockets-17.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:30201a7f69833b015556c72feb69ea501b645986fd0b90dab13f589e995ff428", size = 224615, upload-time = "2026-10-03T14:54:44.41Z" }, + { url = "https://files.pythonhosted.org/packages/87/80/c6f2228ad89774429d270179375ebddb657119215f52d1df7c680d65cad7/websockets-17.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0c8600aec354cc259f1691b0b42816f04a9886a953f82cb227246df76057f97a", size = 225398, upload-time = "2026-10-03T14:54:46.063Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4a/3d8da19732ad468d4be7f1e3ac298078b60bdda55edde6589bef84a5eb7e/websockets-17.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:307fc22ea496be8542d67b82ae8c867a978dfd19ac35573d4f15943fd9277dfe", size = 226571, upload-time = "2026-10-03T14:54:47.672Z" }, + { url = "https://files.pythonhosted.org/packages/58/22/1231657122d9cc24791bb90af13cc2f4e84cf0d3a454cb37e3abfdcb2fd9/websockets-17.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c88697fa943bd4ef67cc919a17d81de6581846f52bfa8c6f64a916098986556", size = 224125, upload-time = "2026-10-03T14:54:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/04/350ca2445da758bc42cdb4218b44d4ce0d5a9c1d5e4cc4a58d64348ad9da/websockets-17.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f7eac84d4969da82166d5e90d9c38d2f416fe24f9708a7013569b193745b9a31", size = 225081, upload-time = "2026-10-03T14:54:51.075Z" }, + { url = "https://files.pythonhosted.org/packages/da/c4/dec952b0df3a5d918ed2a545abb0c25ae519c3bc2d9aba3b7c46abae8f05/websockets-17.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:313f6703023d53baabab6d6c5c37cf637b2c4fee255acf2ed5e92ad69e28f1b7", size = 225376, upload-time = "2026-10-03T14:54:52.675Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b4/198a260afbcc086ff4979774e51834ed7fb5b95f9ef305e0c4924630b857/websockets-17.2-cp314-cp314-win32.whl", hash = "sha256:08d90cf344bdb971ba3a826b78d4da9bfd56cc6a97a604d9b88cbd40bfa6c735", size = 217760, upload-time = "2026-10-03T14:54:54.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9e/0523f8bc2f7aaddf39562d4fa01b4d38fa61b23d980917a16d2dd19c8dac/websockets-17.2-cp314-cp314-win_amd64.whl", hash = "sha256:dac93bf7a9beb215be3282b8441173cd50806c41c007b8be9bb24e03c60ad563", size = 218104, upload-time = "2026-10-03T14:54:55.845Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7b8bb4cb64a199e7082f1f9be784d657842fefc327ac777d6c1493504804/websockets-17.2-cp314-cp314-win_arm64.whl", hash = "sha256:2ab742249f953d148a9ba696c8b9944361e8cb92e8bc61ba2dd53a178403afd3", size = 217989, upload-time = "2026-10-03T14:54:57.376Z" }, + { url = "https://files.pythonhosted.org/packages/ee/76/f54ed054b6e860f1e0bbc7019542a048352d41231fdff6d904b379f881c7/websockets-17.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a69ce25be5f1330ee1c74eb6fabbbceaa96b384beedd2627cecded7546490c40", size = 218125, upload-time = "2026-10-03T14:54:58.943Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4c/0f3375cea66a125ae01d21fb9c537aae955ef499bfe7e2b2376a34362f2a/websockets-17.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8e24b878cf54843a63985d90480f163ca7f692689fbcbe9cdbd8165521083a8b", size = 215658, upload-time = "2026-10-03T14:55:00.674Z" }, + { url = "https://files.pythonhosted.org/packages/0c/05/7c871a67bfb4b61adc1fe13583db97803f87dfeca644fe6ef51df7bb276d/websockets-17.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f33c7908a6885dcae9f462a4a8347b637053b4ff2b96beb4c23fba1cf7818e5f", size = 215858, upload-time = "2026-10-03T14:55:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/41/8e/59df4d9cd357e902d1c74b13c3c0c3841c8df6e4b1b3d131bf26a23fdcb1/websockets-17.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c796a1bb3e4015249639849f30e8e680df8a431b45d417ba8acf843d2451d95f", size = 225443, upload-time = "2026-10-03T14:55:03.966Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/5e486a3a44e041203c62eccf1fc89c7f8824e21104a7b82b182e5b21c228/websockets-17.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:983bcdc898662f6ba9d6a025c30d29946ff0986d9ad60d400af0da3671f7cbf3", size = 225726, upload-time = "2026-10-03T14:55:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/f0/98/b6eb53121c91fbe8b6897aba06861ce60f9ab58faffc6bca5750cbc21681/websockets-17.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35e0f088ddfd9d9bc5019e27ff3767411779e92b59db5bb1507f2731a5b61158", size = 226895, upload-time = "2026-10-03T14:55:07.626Z" }, + { url = "https://files.pythonhosted.org/packages/8a/18/8c091321b99c91eb3eaec9acbd940e69308b4e465b5605c430af0cf7d3a5/websockets-17.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:19e2511412ad3393191de652513bc7a0ca3c93af143b32d96d46e59fbbddf1d4", size = 229040, upload-time = "2026-10-03T14:55:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/1a/96/3a92f944305b7de42fcb7530b9fa69607b4b4ce993c36a9f2330dbc318ba/websockets-17.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb5e2bf969ac99a6ae3c71208a5eb05cfde973192540ffa6e1068b57fb78c4f8", size = 227469, upload-time = "2026-10-03T14:55:10.935Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a9/624f6d75ba326c22d03698b34c0ada984f1d76196322a62f6c22903b831d/websockets-17.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:691780fca2be3dec512cb603cb91060271968cb4af86b51d07c57445c5754a37", size = 226202, upload-time = "2026-10-03T14:55:12.536Z" }, + { url = "https://files.pythonhosted.org/packages/47/af/1e6e8c625aeb268830af2c4227fe05e8db59f4f4debe1dadfd0ada214895/websockets-17.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d39c19b1ba6a6791050383fd69efdd3b63533e2254693d0263879cd5f5921ba", size = 223743, upload-time = "2026-10-03T14:55:14.164Z" }, + { url = "https://files.pythonhosted.org/packages/dd/81/33c5280f4f6f81637c93ae065c6a594dfe35935622af135a5f7c3768bf22/websockets-17.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e48ac2b302986c6f55cf61e8e36b4dd97d0132c5078a713a697a940934ba422e", size = 226492, upload-time = "2026-10-03T14:55:15.796Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/7aa9fc36e67caccbcfee2c48f4ada41e9da512d41523c024d039f0f22ba3/websockets-17.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e136197f1262620ef2e507afc3ea759c1ae7d221886da20eec5f4c9f2618c2aa", size = 224940, upload-time = "2026-10-03T14:55:17.661Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/457aff7081a63d1261608bb4d7b0b0f9dfe780697a2a334671745742850b/websockets-17.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3eb44019a2b0b3b91bac95998f1e4e5589730421170e060fe654a2b7be727dc7", size = 225835, upload-time = "2026-10-03T14:55:19.607Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c3/7a13a3b3050db2c36772ded49f8d48f99eb080948e9f6f762e7529925ab5/websockets-17.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e5855e574804398859c5fbaf4fc7882b96278b7f6572a3d889627e6eb6cfca59", size = 226848, upload-time = "2026-10-03T14:55:21.274Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/d5b2c1e473b1031a4a0ec0e10de69df5b981ab4a10aa482bb45c18dd43f5/websockets-17.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5dc29815520c329f5662f6eb3ebadecf0d4f8c82dfa416d4d6efbf8f39245559", size = 224541, upload-time = "2026-10-03T14:55:22.874Z" }, + { url = "https://files.pythonhosted.org/packages/79/5d/bb81976cc1aa546afb51395ce42913521e9dea062bb34a61308cfff30726/websockets-17.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d1a4f9462da6496b6cb79bbb09c60d17f7e63e8a1df136797b3afabec9560e4d", size = 225315, upload-time = "2026-10-03T14:55:24.443Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/314962d5440c61b4c107914599c13ceeecc6bdb6e2e73a5f7e566a7d1f26/websockets-17.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9496bff5541086478264678bac73c0a75b2fde94fdf6568893bca1f7c6d50d18", size = 225747, upload-time = "2026-10-03T14:55:26.033Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/9eb64b34a3a4458eb08f3f24bde01508f72a00790330723c158ebb965048/websockets-17.2-cp314-cp314t-win32.whl", hash = "sha256:e1e3bc8090a7eae79fdf634b63bdbfa3c93999991023c37c6fd3b469fc8ff5dc", size = 217891, upload-time = "2026-10-03T14:55:27.681Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/3a4e2a09b0822d6e525cbc6e44a4885669bad5b22ab9c64fa2444bc15325/websockets-17.2-cp314-cp314t-win_amd64.whl", hash = "sha256:65a89a5bde227bfe908016f35b5bd347970cd1e5b0360f389502eba1c7fde6e0", size = 218229, upload-time = "2026-10-03T14:55:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/cffb75ee746dd060984c3c3e2eac7f875a866225a30dfa53e2cd18232565/websockets-17.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1c27339934109dfaca83f18ab2c23db06714e9d5deca2c8e37e8f492ab90d20b", size = 218146, upload-time = "2026-10-03T14:55:31.001Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/10a9b1633b63594054c87b97af048628cea2b21b5089a52a9fc1e0af60a3/websockets-17.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:a7c4bb26de6ef496d24822aee4f6a305d97cd33d21a2b85f290292d69ba1c25e", size = 217719, upload-time = "2026-10-03T14:55:32.674Z" }, + { url = "https://files.pythonhosted.org/packages/0c/00/ff4020fe0886dac7199a16ce2805c7afd7b981bd2e81d3fa18dff5d9863a/websockets-17.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c08da1f15040bd1e1a6074bd4518a6ef20e67b1594ecfb0aa75e5b45f87e6d6d", size = 215448, upload-time = "2026-10-03T14:55:34.338Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/bc7b944f81514378b2c2ab96c17df19e871cd33b9be0f1f6dfc975457e5e/websockets-17.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3117abfd32b183bdb6194df9317766d32c6517f3d1c0aa8c62d5c6ccfda0b4a8", size = 215674, upload-time = "2026-10-03T14:55:35.918Z" }, + { url = "https://files.pythonhosted.org/packages/a8/da/2b2b76faa2f10c4813e3872c9577fd13a798f5918b1785b86ff7d635eb2a/websockets-17.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a046227daa7f191e843d26b911c1146233e9a33d249e0c954dcb3ac7c398710e", size = 225119, upload-time = "2026-10-03T14:55:37.777Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d4/22cbe288c0d5cef7620503be92c0098d82220353fc7e188034a19c517240/websockets-17.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2901bdf24f20bc884124b3e88c61f7ece260c20c81e610f2196007395264a4aa", size = 225549, upload-time = "2026-10-03T14:55:39.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0a/504b0d3063679f2c60430c3539482d42a4cb8bd1a76646baf742030a93cc/websockets-17.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60e39adfecf998488166aca8ff24ab1ac406c9ecbecbcf9b3bcfc43cb1ec9a1", size = 226717, upload-time = "2026-10-03T14:55:40.942Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ea/5da9309cc55c2665a6eebc22c369d9918c0d77258c61e92058e6b08d5ff1/websockets-17.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d4df62fd8448a85c752bbea1803cb3a2785e6fc8352009ab64ad7447af079b3c", size = 228413, upload-time = "2026-10-03T14:55:42.54Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/5a24df72aa5500f311105687af864c27f1f9da910e968e97818c6149e6b0/websockets-17.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8eea55fdfa9ba65c6981eea38bd20c800bce2f092a2803d82de764ecf0f071a", size = 227196, upload-time = "2026-10-03T14:55:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ee/ca32cc1ed892dc4ac30a922e8f648048233fbdb8b0bce7048860ec4c60ec/websockets-17.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3f0def1279644acaa9bc861d4234af3f82ea9cee7e460dffac5cb63e691501e9", size = 226092, upload-time = "2026-10-03T14:55:45.842Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/12d4a73324aa9798d5165d20c088f9dba66c75c871960e5d921ec66694e4/websockets-17.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb78fb4158c12f77a934a003006784108a27a6553cfc0c6f10483c9c02e94f48", size = 223486, upload-time = "2026-10-03T14:55:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a4/7fe15da5abb8f0f61e6a357593f7f2ed55724825b7db0ffe72b5c5fad68d/websockets-17.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:f8969ad228115ad8869b5fed801f899e52ab8ad376fdb165ba4760a277c8258a", size = 226200, upload-time = "2026-10-03T14:55:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/4cd3a311f96a2eea0ed458bc01fe2cce42f9cd50aa9e64315dfc855d63a9/websockets-17.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:4a49ca342efc0800e6ae94ed5c9cbdcb319308f75e73c21181e4c24d6710e8dd", size = 224862, upload-time = "2026-10-03T14:55:50.674Z" }, + { url = "https://files.pythonhosted.org/packages/41/b5/22caa3460f75e42bfcc74028870b556d22847ea9a9034aa03986f07f16a9/websockets-17.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:06fa3ce9c3154826c33d4395b225b2994aa64f1f3bcd8be8ed932019175d9268", size = 225391, upload-time = "2026-10-03T14:55:52.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/be/8d28f92092076abf1ddfb3206b0ce956120a22e7c3105f6a3029d727deae/websockets-17.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:50644d8715be7e0ec0682f9d7744b63008e199c5e1618a48fa153756a332235f", size = 226545, upload-time = "2026-10-03T14:55:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/ff943fa383e540fe17f066cc10a3eeedef26e50fd45aae2bdc6746d6f95a/websockets-17.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:60deca33e584c09e91f70f8b55a0b1de7d671d6a63f051d154920f48bed717c7", size = 224352, upload-time = "2026-10-03T14:55:55.856Z" }, + { url = "https://files.pythonhosted.org/packages/e9/df/1e6c3e06c473c9fd833a5c1620b15e2c3b37647b91b7d41871d20bc098de/websockets-17.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b5f79366a8d8dbb981d53ba800bb54a95454595ab8a4548c2b95501b32a08326", size = 225255, upload-time = "2026-10-03T14:55:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/f8/d8a4f988f7cbb568d8bd69da4632c5b6010aa9cd9366f285e23b73b678d9/websockets-17.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f2bbf3f28d0b63157577c8b774b9136f076afa6797e1a52a2ecd477f23cad3a8", size = 225513, upload-time = "2026-10-03T14:55:59.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e0/920357165b2797a2530fc9e271d79a9b5fee2b750b154c990c740f767af3/websockets-17.2-cp315-cp315-win32.whl", hash = "sha256:74836317b7010b579522bb52426f1e225608b042c9e78cbe2493522bebb8a318", size = 217722, upload-time = "2026-10-03T14:56:01.307Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/25bdca25bbc329ffb330ef33993397d6556a871e40a0d196e757699ea3f7/websockets-17.2-cp315-cp315-win_amd64.whl", hash = "sha256:aaead3d926e9ab4124ada727d20cd62d396649917822df4f771d1f07f1079b40", size = 218017, upload-time = "2026-10-03T14:56:02.914Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cb/ea30a552bbcd1c75f0d14bfce6c884ee36187030b85b74a242aacc02406e/websockets-17.2-cp315-cp315-win_arm64.whl", hash = "sha256:40960554e60eb60c3eec4ff9e42a80f84f8cd3ca9bc80a5481a61f1e64d807c9", size = 217929, upload-time = "2026-10-03T14:56:04.604Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/477664c619af8aa3c908d482e2a95e13ceed9d78f21d15902013c3bc6c28/websockets-17.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9a2a60a7f0ea5f239efb6391d2b28630a640d82dad63e3bee47cf2c623c4495d", size = 218029, upload-time = "2026-10-03T14:56:06.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a9/b0be62ff1c0e2bc966da56b36d3d820c7e2ad3c0c4a4ac414fc7335b214f/websockets-17.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:cca2fcb72c007103740fa4fc3df19fdb1a318c641c69f3b0cc47ed63a889336e", size = 215607, upload-time = "2026-10-03T14:56:08.035Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/a6738530de0437a31c1b168e4096ecf790aafaf561f33a009886c7d8042e/websockets-17.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b789356bc4e2e6c20ba52817f92c3fed74e24657654237ecd536c54843b80c6c", size = 215817, upload-time = "2026-10-03T14:56:09.852Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/2fc44ddc419cbb09ee1708af3e78d8a4b018db01fc7e4f91bd730e2f8d9e/websockets-17.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:222fb626fa15701a850eccc778be17312142b2f6a0e16aea80770b7459adb784", size = 225979, upload-time = "2026-10-03T14:56:11.85Z" }, + { url = "https://files.pythonhosted.org/packages/2e/91/a215b14caa7ea65bc36db81609108899c259503300d1560dae9c70a135e7/websockets-17.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4497e87c34a2d21cbec1227858fec3af8e514dd70c47625557a122fcebc081dc", size = 226250, upload-time = "2026-10-03T14:56:13.548Z" }, + { url = "https://files.pythonhosted.org/packages/65/b9/9406a18e9edf558ed504d2a7679371d0f8107e4ef526c80b154ea4ec9752/websockets-17.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6281c171557ce0e408e19d9a223f22d915117ac38a5a7f32ed83809e7492316c", size = 227579, upload-time = "2026-10-03T14:56:15.143Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/a73af119244f46f5130005d7ab63f1c75890c890141a0ca2adc9d97d4671/websockets-17.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:08d97098644728bd1895caa7ecf3090b8e563d70809870d2adb33a107bd061d0", size = 229205, upload-time = "2026-10-03T14:56:17.086Z" }, + { url = "https://files.pythonhosted.org/packages/c1/92/ccd8e2e921d134a56f1ed4642d276500d9e33b3dc4d6deb63d614b3e53a6/websockets-17.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1fdb8d5a1660307dc6d36d0b7fc725213cbd7f80800904dc4896aa3208b89121", size = 228011, upload-time = "2026-10-03T14:56:18.716Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ef/7d71105d19a7aaab5ff87b9c712f6c1dda44e72ea56aa0e7b777f2fc274b/websockets-17.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18b0a46e5e9b315e2b54ce8c3bafdeef0e1388ca363114fa868e6aab2dc58512", size = 226892, upload-time = "2026-10-03T14:56:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/56/f7/87012d628b21e66e699440f39bfa7cc55fae7f52b2c532ab62184a589624/websockets-17.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f115d5d804a2163dd89245710049078b0e726a58c1f44a1f86c2c6e79055d76", size = 224241, upload-time = "2026-10-03T14:56:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/55/f5/495371068b27ee5f7c435187f9dafd62402f195e2c76063bdd4653da1565/websockets-17.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:1d829946a2e7630f92f9d7b45b62f3abe9f393cc2dea6a35edb3988f865e75f2", size = 227076, upload-time = "2026-10-03T14:56:23.909Z" }, + { url = "https://files.pythonhosted.org/packages/18/18/3dce3cc6099be5e044e0fd5d0e0c9931c8e3387511cdec8014a345f619e5/websockets-17.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:6c274fc1572edf7c197094a0eb1887d45fdc95254bc80597dc7599550486c06a", size = 225727, upload-time = "2026-10-03T14:56:25.689Z" }, + { url = "https://files.pythonhosted.org/packages/47/30/57d0c7aaf8d4473926fa8829b8136483f561388d1e747ae71c9f2a83d5fd/websockets-17.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:4173a4b8a025ae44313d9d9b4ecf31e886c7b7faf45386d51a8ca4ff2dcf3f2a", size = 226225, upload-time = "2026-10-03T14:56:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9f/9dce1203756756c00b407b9a6b13a7500fcd38f2634d4daa3f65575814ec/websockets-17.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:d8cfe9522ad69b6abb26b413ed1deca43cb915cefc588433d557cb3ae1c783e2", size = 227333, upload-time = "2026-10-03T14:56:28.811Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2f/d3b6b876678ebb03017b7afd7111fe44d54b93f036a80ebb4b481dd1ab74/websockets-17.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:908d81d88bb16141613a6275059b5114656d5c2f0b5400b421d54fe6f1943507", size = 225082, upload-time = "2026-10-03T14:56:30.578Z" }, + { url = "https://files.pythonhosted.org/packages/32/b0/a69b573a5e56d2e7a5dcbb447466f442380cf81515e1cb1220cd626c8042/websockets-17.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c6590e1eb624ff6b15b872421bc9a10bc6d2057635d69c6cd244ac3f928f85c6", size = 225945, upload-time = "2026-10-03T14:56:32.32Z" }, + { url = "https://files.pythonhosted.org/packages/70/be/a72911dc8e33f74c196012366ce4d99b1a803894a377a1ed0c8e66df9caa/websockets-17.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:61040f6f7da5a279d2f77496c69d51132aba75f701c52bded400d4c639277b18", size = 226241, upload-time = "2026-10-03T14:56:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a9/02a68c1d8e5572918e0962d3aad881078f73ede43abd9b1336e4efaa8909/websockets-17.2-cp315-cp315t-win32.whl", hash = "sha256:f90bad2839c185a1edf8ee22a257cfc8a39e0e337a0490ab185dfa76ef04d1bd", size = 217847, upload-time = "2026-10-03T14:56:36.204Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/3d7c33b8d5e7712a60e0149c017ed50394ec5e8cf72e5cb6a1ffaf11a42d/websockets-17.2-cp315-cp315t-win_amd64.whl", hash = "sha256:315551f4ccedbbf9fd4f7e8bf037a5948c976ade0e919ba5d8f581d465f6f725", size = 218169, upload-time = "2026-10-03T14:56:37.79Z" }, + { url = "https://files.pythonhosted.org/packages/27/57/ab34cc6460c5322e6932750fa5c6c64be89e6ee4e2707d13c4e9d3312b25/websockets-17.2-cp315-cp315t-win_arm64.whl", hash = "sha256:0a6220bdf8d5f11af71251a599092d89ac1d6bfac691c7f5951c5b07953947a0", size = 218089, upload-time = "2026-10-03T14:56:39.427Z" }, + { url = "https://files.pythonhosted.org/packages/7f/e2/09ad9cec0fc7e39f983b52f9e49c44f89b7cf7a61d4761fa7fc398f003f9/websockets-17.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2de1ccf298f5c9e0f27113836d742edb95f015eee3148f004ac386f7ba9a05b1", size = 215348, upload-time = "2026-10-03T14:56:41.037Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/c307b5d8cdf1852d00606a0403502f0ca5cd8a4736550bab70abce09f7e9/websockets-17.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:761cde41439f0be761aa460e1451a31e2e14baf4a46db6fe4913e5a06a90df66", size = 215621, upload-time = "2026-10-03T14:56:43.097Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/af8412f154cd0568afc043ab478cc8c1ebdf9337b25c85cb9a049d18cfcb/websockets-17.2-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:15a7101b660a9f15fac34108c92cefc9848f6753a50acef8869e3cd94148fdb7", size = 216569, upload-time = "2026-10-03T14:56:44.979Z" }, + { url = "https://files.pythonhosted.org/packages/fc/76/92ae57b985378036bb8133ea39d1e5cc4d97accad9cae38169426bdcef75/websockets-17.2-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:214da56dba368f61b3d745c77630b2d03c61c02da7b42fe80ef6efba079d3077", size = 216462, upload-time = "2026-10-03T14:56:46.771Z" }, + { url = "https://files.pythonhosted.org/packages/e5/35/e3b276473f7f38984990eb29cf525ffaed131f6136bedb929b5c2ce7151e/websockets-17.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80cbc645af23ac5c12096545c161626960114a1bc10f864760558d3b3e82ba18", size = 217355, upload-time = "2026-10-03T14:56:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a1/459ab96c5cda8a2164f594be6dc9f868de7971e6abafa696ea07534139a6/websockets-17.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:063508ce9e0db745f30ab52fc652f4e59efc79c2b74934b3837d5cdb974da620", size = 218612, upload-time = "2026-10-03T14:56:50.287Z" }, + { url = "https://files.pythonhosted.org/packages/8a/58/835cd51934d6780fa586f275b5d9901eead6d81569b4343b3767cdbaae4c/websockets-17.2-py3-none-any.whl", hash = "sha256:6aa59f0ef92e796b2db6f5f26550c4713c0e4036899fadf02f55e2ed4db0b7ae", size = 211883, upload-time = "2026-10-03T14:56:51.898Z" }, +]