diff --git a/pyproject.toml b/pyproject.toml index d96fd965..332d75ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ [project.optional-dependencies] dev = [ + "hypothesis>=6.100,<7", "pytest>=8.0.0,<9", "ruff>=0.5.0", ] @@ -56,5 +57,6 @@ target-version = "py311" [dependency-groups] dev = [ + "hypothesis>=6.100,<7", "pytest>=8.0.0,<9", ] diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index de6f4380..80186a78 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -1436,61 +1436,81 @@ def _registry_event( } -def build_catalog( - observations_path: pathlib.Path, - docket_path: pathlib.Path | None, - existing: ExistingCatalog, - registry: UuidRegistry, -) -> tuple[dict, dict]: - """Build the catalog and the identity plan. +def _append_gate_helpers(): + """Import the append gate's supersede-aware view and content address.""" + try: + from check_thesis_facts_append import ( + effective_current_rows, + expected_assertion_version_id, + ) + except ImportError as exc: # pragma: no cover - environment guard + raise SystemExit( + "cannot import the append gate's content address and " + "supersede-aware current view from " + f"check_thesis_facts_append (receipt package required): {exc}" + ) + return effective_current_rows, expected_assertion_version_id - The plan records every registry-affecting outcome: ``mints`` (new - identity bindings to append), ``supersedes`` (identities whose UUID - changes — these require ``--allow-remint``), and ``dropped`` (existing - rows whose UUID would vanish from the catalog — also gated). + +def effective_assertion_version_id(row: dict, index: int) -> str: + """Return the assertion version id the append gate assigns ``row``. + + Mirrors receipt's ``_effective_assertion_id``: a versioned row is + addressed by its explicit ``assertionVersion.id``, and a row with no + ``assertionVersion`` (every row of the immutable prefix predates + versioning) by its recomputed av2 content address. A correction of a + prefix row names that address in ``supersedes``. ``index`` only + labels refusals. """ - raw = observations_path.read_bytes() - observation_lines = raw.decode().split("\n") - if observation_lines and observation_lines[-1] == "": - observation_lines.pop() - rows = [ - json.loads( - line, - object_pairs_hook=_reject_duplicate_keys, - parse_constant=_reject_json_constants, + version = row.get("assertionVersion") + if version is None: + _, content_address = _append_gate_helpers() + try: + return content_address(row) + except (AttributeError, TypeError, ValueError) as exc: + raise SystemExit( + f"observation row {index}: cannot compute the content " + f"address of a row without assertionVersion: {exc}" + ) from None + if not isinstance(version, dict) or not isinstance( + version.get("id"), str + ) or not version["id"]: + raise SystemExit( + f"observation row {index}: assertionVersion must carry a " + "nonempty string id" ) - for line in observation_lines - if line.strip() - ] - # The journal is append-only: a correction appends a row whose - # assertionVersion.supersedes names the version it replaces. Series - # identity must follow the supersede-aware CURRENT view (the same one - # the ledger's aggregate-fact validation uses), or superseded - # assertions would keep stale identities alive forever. The imported - # helper assumes append-gate-validated input; standalone runs get the - # same preconditions enforced here (unique ids, resolvable links, no - # cycles). - seen_ids: dict[str, int] = {} + return version["id"] + + +def check_assertion_versions(rows: list[dict]) -> list[str]: + """Enforce the append gate's supersede preconditions; return every id. + + The gate (receipt ``check_rows``) reserves the effective id of EVERY + row, versioned or not, and a correction names the effective id of the + version it replaces. This check reserves the same ids, so a + gate-accepted correction of a prefix row is accepted here too. It + refuses the part of the gate's rules the current view relies on: two + rows with one effective id (identical unversioned rows, or a row + restating an earlier row's exact addressed content), a link to no + row's id, a self-link and a cycle. The gate refuses each of these as + well. Its other rules (an explicit id equals the content address; a + link names the active version of the same record, on an earlier line, + so no version is superseded twice) are the gate's alone. + """ + ids: list[str] = [] + owner: dict[str, int] = {} links: dict[str, str] = {} for index, row in enumerate(rows): - version = row.get("assertionVersion") - if version is None: - continue - if not isinstance(version, dict) or not isinstance( - version.get("id"), str - ) or not version["id"]: - raise SystemExit( - f"observation row {index}: assertionVersion must carry a " - "nonempty string id" - ) - vid = version["id"] - if vid in seen_ids: + vid = effective_assertion_version_id(row, index) + if vid in owner: raise SystemExit( - f"observation row {index}: assertionVersion id {vid!r} " - f"duplicates row {seen_ids[vid]}" + f"observation row {index}: assertion version {vid!r} " + f"duplicates row {owner[vid]}" ) - seen_ids[vid] = index - supersedes = version.get("supersedes") + owner[vid] = index + ids.append(vid) + version = row.get("assertionVersion") + supersedes = version.get("supersedes") if version is not None else None if supersedes is not None: if not isinstance(supersedes, str) or not supersedes: raise SystemExit( @@ -1504,7 +1524,7 @@ def build_catalog( ) links[vid] = supersedes for vid, target in links.items(): - if target not in seen_ids: + if target not in owner: raise SystemExit( f"assertionVersion {vid!r} supersedes unknown version " f"{target!r}" @@ -1519,13 +1539,44 @@ def build_catalog( ) seen_chain.add(cursor) cursor = links.get(cursor) - try: - from check_thesis_facts_append import effective_current_rows - except ImportError as exc: # pragma: no cover - environment guard - raise SystemExit( - "cannot import the supersede-aware current view from " - f"check_thesis_facts_append (receipt package required): {exc}" + return ids + + +def build_catalog( + observations_path: pathlib.Path, + docket_path: pathlib.Path | None, + existing: ExistingCatalog, + registry: UuidRegistry, +) -> tuple[dict, dict]: + """Build the catalog and the identity plan. + + The plan records every registry-affecting outcome: ``mints`` (new + identity bindings to append), ``supersedes`` (identities whose UUID + changes — these require ``--allow-remint``), and ``dropped`` (existing + rows whose UUID would vanish from the catalog — also gated). + """ + raw = observations_path.read_bytes() + observation_lines = raw.decode().split("\n") + if observation_lines and observation_lines[-1] == "": + observation_lines.pop() + rows = [ + json.loads( + line, + object_pairs_hook=_reject_duplicate_keys, + parse_constant=_reject_json_constants, ) + for line in observation_lines + if line.strip() + ] + # The journal is append-only: a correction appends a row whose + # assertionVersion.supersedes names the version it replaces. Series + # identity must follow the supersede-aware CURRENT view (the same one + # the ledger's aggregate-fact validation uses), or superseded + # assertions would keep stale identities alive forever. The imported + # helper assumes append-gate-validated input; standalone runs get the + # preconditions it relies on from check_assertion_versions. + check_assertion_versions(rows) + effective_current_rows, _ = _append_gate_helpers() current_rows = effective_current_rows(rows) identities = build_identities(current_rows) diff --git a/tests/test_build_series_catalog.py b/tests/test_build_series_catalog.py index 584cb707..fc886040 100644 --- a/tests/test_build_series_catalog.py +++ b/tests/test_build_series_catalog.py @@ -10,17 +10,28 @@ from __future__ import annotations +import copy import doctest +import hashlib import json import pathlib +import re import sys import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +# receipt's effective-id rule (private; effective_current_rows uses it, and +# check_rows reserves the same id inline after requiring an explicit id to +# equal the content address): the differential oracle for the generator's. +from receipt.append_gate import _effective_assertion_id ROOT = pathlib.Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) import build_series_catalog as bsc # noqa: E402 +import check_thesis_facts_append as gate # noqa: E402 PERIOD_SEGMENTS = [ "fy2026", @@ -782,6 +793,10 @@ def test_committed_catalog_is_current_and_valid() -> None: # Occurrences as of 55bbf3d: both August keys map to # dol.eta.continued_claims.sa (joined week_2026-08-15 at d77afe2) and # us.dol.initial_claims.sa. + # Extended 2026-09-29 to the catalog at 598f394, whose eight-row + # first-print append first spelled week_2026-08-29 and week_2026-09-05 + # (both claims series) and week_2026-09-12 (us.dol.initial_claims.sa + # only). Each is the row's own week_ending period, so the strips stand. # EVERY stripped spelling is auditable, mapped to the canonical # concepts it touched — a statute or edition label colliding with a # period spelling can only be caught here. @@ -794,7 +809,8 @@ def test_committed_catalog_is_current_and_valid() -> None: "week_2026-06-20", "week_2026-06-27", "week_2026-07-04", "week_2026-07-11", "week_2026-07-13", "week_2026-07-18", "week_2026-07-25", "week_2026-08-01", "week_2026-08-08", - "week_2026-08-15", "week_2026-08-22", "week_2026_06_13", + "week_2026-08-15", "week_2026-08-22", "week_2026-08-29", + "week_2026-09-05", "week_2026-09-12", "week_2026_06_13", "week_ending_2026_06_06", ] assert committed["stripped_segments"]["after_mpc_june_2026"] == [ @@ -2188,6 +2204,409 @@ def test_assertion_version_preconditions(tmp_path: pathlib.Path) -> None: _build(tmp_path, [f]) +# The append gate (receipt check_rows) gives EVERY journal row one effective +# assertion version id: its explicit assertionVersion.id, or, for a row +# without one (the whole immutable prefix), its recomputed av2 content +# address. It reserves every such id and a correction names one of them. +# The generator's preconditions must accept whatever the gate accepts; on +# journals whose explicit ids are their content addresses (the gate's first +# row rule), refuse a duplicate id, a self-link, an unresolvable link and a +# cycle exactly when the gate refuses for that reason; and with any defects +# at all, refuse only what the gate refuses. 2026-09-29 repro: six +# corrections of the June 2026 Table A-19 prefix rows passed the gate and +# were refused here as superseding an "unknown version", which would have +# failed every later resolver append. + +PREFIX_LINE_COUNT = json.loads( + (ROOT / "ledger" / "immutable_prefix.json").read_text(encoding="utf-8") +)["prefixLineCount"] + + +def _address(row: dict) -> str: + return _effective_assertion_id(row, gate.APPEND_GATE_SPEC) + + +def _journal_row( + record: str, + value: float, + supersedes: str | None = None, + *, + versioned: bool = True, +) -> dict: + """A row check_rows accepts; ``versioned=False`` is a prefix-style row.""" + row = _row("agency.rate", rid=record) + row["value"] = value + row.update( + retrievedAt="2026-06-01T00:00:00Z", + sourceVintage="first_print", + ledgerRepoSha="0" * 40, + responseArchive={"sha256": "0" * 64}, + ) + if versioned: + # The content address excludes the link, so relinking keeps the id. + row["assertionVersion"] = { + "id": gate.expected_assertion_version_id(row), + "supersedes": supersedes, + } + return row + + +def _gate_refusal(rows: list[dict], prefix_count: int) -> str | None: + try: + gate.check_rows([json.dumps(row) for row in rows], prefix_count) + except gate.AppendError as exc: + return str(exc) + return None + + +GENERATOR_REASONS = { + "duplicates row": "duplicate", + "supersedes itself": "self", + "supersedes unknown version": "unknown", + "supersede cycle": "cycle", +} + + +def _generator_refusal(rows: list[dict]) -> str | None: + """The generator's refusal reason, or its raw message if unclassified.""" + try: + bsc.check_assertion_versions(rows) + except SystemExit as exc: + message = str(exc) + return next( + (reason for text, reason in GENERATOR_REASONS.items() + if text in message), + message, + ) + return None + + +GATE_LINK_REFUSAL = re.compile(r"line (\d+) supersedes (\S+) but ") + + +def _gate_reason(rows: list[dict], refusal: str | None) -> str | None: + """Classify the gate's refusal by the generator's reasons. + + The gate names only the first line it refuses. A restated id is a + duplicate. A refused link is classified by where its target lies among + the gate's own effective ids: this row (self), no row (unknown), or a + row whose chain leads back here (cycle). Every other refusal (a link to + a non-active, other-record or later version outside a cycle, a missing + link) is a rule the gate alone enforces, which the generator must + accept: None. So is an explicit id that is not the content address, + but the generator trusts explicit ids, so such a journal can still be + refused there for a duplicate or orphaned link; journals with one are + held only to containment. + """ + if refusal is None: + return None + if " restates assertion version " in refusal: + return "duplicate" + match = GATE_LINK_REFUSAL.match(refusal) + if match is None: + return None + ids = [_address(row) for row in rows] + own, target = ids[int(match[1]) - 1], match[2] + if target == own: + return "self" + if target not in ids: + return "unknown" + links = { + ids[index]: (row.get("assertionVersion") or {}).get("supersedes") + for index, row in enumerate(rows) + } + cursor, seen = target, set() + while cursor is not None and cursor not in seen: + if cursor == own: + return "cycle" + seen.add(cursor) + cursor = links.get(cursor) + return None + + +def _relink(row: dict, target: str | None) -> None: + row["assertionVersion"]["supersedes"] = target + + +def test_live_ledger_reserves_the_gates_ids() -> None: + # Regression on the live ledger: the gate accepts it, and every row, + # the prefix rows without assertionVersion included, gets the gate's + # effective id. test_committed_catalog_is_current_and_valid pins the + # catalog bytes, which this change leaves identical. + lines = bsc.OBSERVATIONS.read_text(encoding="utf-8").splitlines() + rows = [json.loads(line) for line in lines] + assert all( + "assertionVersion" not in row for row in rows[:PREFIX_LINE_COUNT] + ) + gate.check_rows(lines, PREFIX_LINE_COUNT) + assert bsc.check_assertion_versions(rows) == [_address(r) for r in rows] + + +def test_correction_of_a_prefix_row_is_accepted_like_the_gate( + tmp_path: pathlib.Path, +) -> None: + # The 2026-09-29 repro on the live ledger. SYNTHETIC corrections: the + # restatement in millions is the repro's, not a chosen remedy (that is + # decision d397), and each archive digest is synthetic so no real + # correction can share its content address. Each supersedes the active + # version of its June record, which is the prefix row's av2 address + # until a real correction lands. + lines = bsc.OBSERVATIONS.read_text(encoding="utf-8").splitlines() + rows = [json.loads(line) for line in lines] + june = [ + row for row in rows[:PREFIX_LINE_COUNT] + if row["source_record_id"].startswith( + "bls.cps.employed_people_by_occupation." + ) + and row["source_record_id"].endswith(".june_2026.first_print") + ] + assert len(june) == 6 + assert all("assertionVersion" not in row for row in june) + corrections = [] + for original in june: + record = original["source_record_id"] + active = [r for r in rows if r["source_record_id"] == record][-1] + correction = copy.deepcopy(original) + correction["value"] = round(original["value"] / 1000, 3) + correction["measure"]["unit"] = "millions" + correction["responseArchive"]["sha256"] = hashlib.sha256( + f"synthetic correction of {record}".encode() + ).hexdigest() + correction["assertionVersion"] = { + "id": gate.expected_assertion_version_id(correction), + "supersedes": _address(active), + } + corrections.append(correction) + journal = rows + corrections + + assert _gate_refusal(journal, PREFIX_LINE_COUNT) is None + ids = bsc.check_assertion_versions(journal) + assert ids == [_address(row) for row in journal] + + observations = tmp_path / "obs.jsonl" + observations.write_text( + "".join(json.dumps(row) + "\n" for row in journal), encoding="utf-8" + ) + catalog, plan = bsc.build_catalog( + observations, + bsc.DOCKET_SEED, + bsc.ExistingCatalog(bsc.CATALOG), + bsc.UuidRegistry.load(bsc.UUID_REGISTRY), + ) + assert not plan["mints"] and not plan["supersedes"] and not plan["dropped"] + occupations = [ + series for series in catalog["series"] + if series["concept"].startswith( + "bls.cps.employed_people_by_occupation." + ) + ] + assert len(occupations) == 6 + assert all(series["unit"] == "millions" for series in occupations) + assert catalog["observation_rows"] == len(journal) + # Each correction replaces its target, so the current count is the + # live ledger's. + assert catalog["current_assertion_rows"] == len( + gate.effective_current_rows(rows) + ) + + +def test_every_rows_effective_id_is_reserved() -> None: + # A row without assertionVersion reserves its content address exactly + # as the gate does, so two identical such rows, a versioned re-append + # of one, and an A -> B -> A restore of one all collide. So does an + # A -> B -> A restore among versioned rows. + legacy = _journal_row("rec.a", 1.0, versioned=False) + reissued = copy.deepcopy(legacy) + reissued["assertionVersion"] = {"id": _address(legacy), "supersedes": None} + b = _journal_row("rec.a", 2.0, _address(legacy)) + restore = copy.deepcopy(legacy) + restore["assertionVersion"] = { + "id": _address(legacy), "supersedes": _address(b) + } + a2 = _journal_row("rec.b", 1.0) + b2 = _journal_row("rec.b", 2.0, _address(a2)) + back = _journal_row("rec.b", 1.0, _address(b2)) + for journal, prefix, match in ( + ([legacy, copy.deepcopy(legacy)], 2, "row 1: .* duplicates row 0"), + ([legacy, reissued], 1, "row 1: .* duplicates row 0"), + ([legacy, b, restore], 1, "row 2: .* duplicates row 0"), + ([a2, b2, back], 0, "row 2: .* duplicates row 0"), + ): + assert _gate_reason(journal, _gate_refusal(journal, prefix)) == ( + "duplicate" + ) + with pytest.raises(SystemExit, match=match): + bsc.check_assertion_versions(journal) + + +def test_link_refusals_fire_where_the_gate_refuses() -> None: + legacy = _journal_row("rec.a", 1.0, versioned=False) + unknown = _journal_row("rec.a", 2.0, "av2:" + "f" * 64) + # A near miss: the address the prefix row would have at another value. + near = _journal_row("rec.a", 2.0, _address(dict(legacy, value=3.0))) + itself = _journal_row("rec.a", 2.0) + _relink(itself, _address(itself)) + x, y = (_journal_row(f"rec.{n}", 1.0) for n in "xy") + _relink(x, _address(y)) + _relink(y, _address(x)) + two_cycle = [legacy, x, y] + p, q, r = (_journal_row(f"rec.{n}", 1.0) for n in "pqr") + _relink(p, _address(q)) + _relink(q, _address(r)) + _relink(r, _address(p)) + three_cycle = [legacy, p, q, r] + for journal, match, reason in ( + ([legacy, unknown], "supersedes unknown version", "unknown"), + ([legacy, near], "supersedes unknown version", "unknown"), + ([legacy, itself], "supersedes itself", "self"), + (two_cycle, "supersede cycle", "cycle"), + (three_cycle, "supersede cycle", "cycle"), + ): + assert _gate_reason(journal, _gate_refusal(journal, 1)) == reason + with pytest.raises(SystemExit, match=match): + bsc.check_assertion_versions(journal) + + +@st.composite +def _gate_valid_journals(draw) -> tuple[list[dict], int]: + """A journal check_rows accepts: prefix-style rows without + assertionVersion, then versioned appends, each a new record or a + correction of a record's active version (a prefix row's by its + content address).""" + prefix = draw(st.integers(0, 4)) + steps = draw(st.lists(st.one_of(st.none(), st.integers(0, 7)), max_size=8)) + rows: list[dict] = [] + active: dict[str, str] = {} + for _ in range(prefix): + row = _journal_row(f"rec.{len(active)}", float(len(rows)), + versioned=False) + rows.append(row) + active[row["source_record_id"]] = _address(row) + for step in steps: + if step is None or not active: + record, supersedes = f"rec.{len(active)}", None + else: + record = sorted(active)[step % len(active)] + supersedes = active[record] + row = _journal_row(record, float(len(rows)), supersedes) + rows.append(row) + active[record] = _address(row) + return rows, prefix + + +MUTATIONS = ( + "unknown", "self", "cycle", "relink", "unlink", "duplicate", "restore" +) +# An explicit id that is not the row's content address: the gate refuses +# it outright, and only containment is claimed for it. +ANY_MUTATION = (*MUTATIONS, "reid") + + +def _mutate(draw, kind: str, rows: list[dict], prefix: int): + """Apply one defect. Only ``supersedes`` changes, which leaves every id + alone, or a row is added: a copy of any row, or an A -> B -> A + restore. The exception is "reid", which rewrites one explicit id to + another row's id or to an address no row has.""" + ids = [_address(row) for row in rows] + versioned = [i for i, row in enumerate(rows) if "assertionVersion" in row] + if kind == "unknown" and versioned: + target = "av2:" + hashlib.sha256( + draw(st.binary(min_size=1, max_size=8)) + ).hexdigest() + _relink(rows[draw(st.sampled_from(versioned))], target) + elif kind == "self" and versioned: + i = draw(st.sampled_from(versioned)) + _relink(rows[i], ids[i]) + elif kind == "cycle" and len(versioned) >= 2: + ring = sorted(draw(st.lists(st.sampled_from(versioned), min_size=2, + max_size=3, unique=True))) + for a, b in zip(ring, ring[1:] + ring[:1]): + _relink(rows[a], ids[b]) + elif kind == "relink" and versioned and len(rows) >= 2: + # Any other row, earlier or later: the valid target, a stale or + # other-record version, or a forward link that may close a cycle. + i = draw(st.sampled_from(versioned)) + j = draw(st.sampled_from([k for k in range(len(rows)) if k != i])) + _relink(rows[i], ids[j]) + elif kind == "unlink" and versioned: + _relink(rows[draw(st.sampled_from(versioned))], None) + elif kind == "duplicate" and rows: + j = draw(st.integers(0, len(rows) - 1)) + at = draw(st.integers(j + 1, len(rows))) + rows.insert(at, copy.deepcopy(rows[j])) + prefix += at < prefix + elif kind == "restore": + versions: dict[str, list[int]] = {} + for i, row in enumerate(rows): + versions.setdefault(row["source_record_id"], []).append(i) + chains = [v for _, v in sorted(versions.items()) if len(v) >= 2] + if chains: + chain = draw(st.sampled_from(chains)) + first = copy.deepcopy(rows[chain[0]]) + first["assertionVersion"] = { + "id": ids[chain[0]], "supersedes": ids[chain[-1]] + } + rows.append(first) + elif kind == "reid" and versioned: + i = draw(st.sampled_from(versioned)) + stray = "av2:" + hashlib.sha256( + draw(st.binary(min_size=1, max_size=8)) + ).hexdigest() + rows[i]["assertionVersion"]["id"] = draw(st.sampled_from([*ids, stray])) + return rows, prefix + + +@st.composite +def _mutated_journals(draw, max_mutations: int, kinds=MUTATIONS): + rows, prefix = draw(_gate_valid_journals()) + rows = copy.deepcopy(rows) + kinds = draw(st.lists(st.sampled_from(kinds), max_size=max_mutations)) + for kind in kinds: + rows, prefix = _mutate(draw, kind, rows, prefix) + return rows, prefix + + +PROPERTY_SETTINGS = settings( + max_examples=400, deadline=None, derandomize=True, database=None +) + + +@PROPERTY_SETTINGS +@given(_gate_valid_journals()) +def test_gate_accepted_journals_pass_the_preconditions(journal) -> None: + # Invariant: the gate accepts => the generator accepts, and both assign + # every row the same effective id. + rows, prefix = journal + assert _gate_refusal(rows, prefix) is None + assert bsc.check_assertion_versions(rows) == [_address(r) for r in rows] + + +@PROPERTY_SETTINGS +@given(_mutated_journals(max_mutations=1)) +def test_generator_refuses_iff_the_gate_does_for_that_reason(journal) -> None: + # Invariant, on journals with at most one defect: the generator refuses + # for reason R exactly when the gate's refusal classifies as R, and + # accepts every journal the gate accepts or refuses for its own rules. + rows, prefix = journal + refusal = _gate_refusal(rows, prefix) + assert _generator_refusal(rows) == _gate_reason(rows, refusal), refusal + + +@PROPERTY_SETTINGS +@given(_mutated_journals(max_mutations=3, kinds=ANY_MUTATION)) +def test_generator_refusals_are_gate_refusals(journal) -> None: + # Invariant, with several defects, ids rewritten away from their content + # addresses included: every generator refusal is a gate refusal, and a + # gate refusal for a generator reason is refused here. + rows, prefix = journal + refusal = _gate_refusal(rows, prefix) + generator = _generator_refusal(rows) + if generator is not None: + assert refusal is not None + if _gate_reason(rows, refusal) is not None: + assert generator is not None + + def test_cross_dimension_ambiguous_alias_never_steals( tmp_path: pathlib.Path, ) -> None: diff --git a/uv.lock b/uv.lock index 3a5807ed..1d609318 100644 --- a/uv.lock +++ b/uv.lock @@ -635,6 +635,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, ] +[[package]] +name = "hypothesis" +version = "6.168.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/b7/13118bbc45d6d8b9d04e2de779e2a4ff23145ea39efa692b994fb874ca72/hypothesis-6.168.3.tar.gz", hash = "sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75", size = 511022, upload-time = "2026-09-28T05:20:58.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/cd/746a2fc1e5e5ba34f07ea6ee1ad07525a3dde5ff4836db3a7980af49a891/hypothesis-6.168.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb", size = 791536, upload-time = "2026-09-28T05:18:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7a158e377b69556c8e12c25c8fd811d0103e54c24a12dab1f3b9819f202e/hypothesis-6.168.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c", size = 787313, upload-time = "2026-09-28T05:20:18.122Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/7df9104ee359e8fcd77791781c47e70794964ca69673665e0de2a6590f4c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e", size = 1124036, upload-time = "2026-09-28T05:20:04.372Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/8a61715404a73b9a82cb1f976803428d603cdea976a9c30266c72ad6a7ce/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7", size = 1147875, upload-time = "2026-09-28T05:19:15.495Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3f/2b16e95cc3b9a069afa23830a012aefe377ce4a457a227e5a5026eb827ff/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d", size = 1149489, upload-time = "2026-09-28T05:19:18.886Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/d7cf6dc068bb02732b2a6e38a35a0dcb7f2137608198fc79d740f69d197c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c", size = 1191992, upload-time = "2026-09-28T05:19:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/f1e5b25dec15998e8375d722825ec3dae1075cbe7cb1f74221b2312a2e33/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef", size = 1169911, upload-time = "2026-09-28T05:19:27.996Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/901234a49147e6bf8a5b9e54b775706a223c2b52befea1106ea2aaf15d48/hypothesis-6.168.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef", size = 1129406, upload-time = "2026-09-28T05:18:42.632Z" }, + { url = "https://files.pythonhosted.org/packages/55/c1/5fc9ff91ec9fba6bb527833386c66812524b5cc75fe7d4a4f9122ee1c420/hypothesis-6.168.3-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd", size = 1160399, upload-time = "2026-09-28T05:20:23.911Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/49ad1ef5c55547c8abfd0fc571b3493806dda530fd5cb3d8263dbad86a72/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595", size = 1299906, upload-time = "2026-09-28T05:18:58.417Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/f98a26094c4f462c4215807bed0f9a6b5508b27a3c329bcba1bc8adc7ac7/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11", size = 1425538, upload-time = "2026-09-28T05:20:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b8/c5b4406e3a6591e51a42f85469351d1d824e0a54b84167aec9b2353759fe/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab", size = 1377084, upload-time = "2026-09-28T05:20:06.618Z" }, + { url = "https://files.pythonhosted.org/packages/e0/01/0d84a8ea469d024c15f602799c0b8410fbe03d99fdb0370449b33c3c916f/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd", size = 1281162, upload-time = "2026-09-28T05:19:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/22/b5/ea6435038de1a795f005cb603531e8ccc053febc50180f3ab55583c9c60e/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef", size = 1300433, upload-time = "2026-09-28T05:18:55.634Z" }, + { url = "https://files.pythonhosted.org/packages/6c/78/fa6c77d9f64b6d592e69e582d83cbce7d7e56897faa4762885a2123b0166/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026", size = 1336273, upload-time = "2026-09-28T05:19:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/f1/56/4acd0d778bab818c9ea336fcba768bf43bc70db7f84cc373579298df4641/hypothesis-6.168.3-cp310-abi3-win32.whl", hash = "sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4", size = 678211, upload-time = "2026-09-28T05:19:23.355Z" }, + { url = "https://files.pythonhosted.org/packages/b0/db/0a02b146ad1c30f9716362f2e1a379dfd6b0f7be03ac1e22c9da24c9e2a9/hypothesis-6.168.3-cp310-abi3-win_amd64.whl", hash = "sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24", size = 684906, upload-time = "2026-09-28T05:18:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c0/958deaf726848f96f52250740bf39f13f476b068e22f73e2b358eaa07532/hypothesis-6.168.3-cp310-abi3-win_arm64.whl", hash = "sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a", size = 683292, upload-time = "2026-09-28T05:19:57.404Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ab/443208c2988a24eaafbfbd6791b897ad9a8a1dd7f870d5b455d7d15a28ad/hypothesis-6.168.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b", size = 792239, upload-time = "2026-09-28T05:19:59.144Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7b/619c1a76c52de2d3cfd68a2f9876c09464d63097db5bf86a28b62dc37bb1/hypothesis-6.168.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102", size = 788113, upload-time = "2026-09-28T05:19:17.145Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/33841e8833105f850214a486d83740ba52041892ca94e687a05829c0ce83/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b", size = 1124241, upload-time = "2026-09-28T05:18:37.589Z" }, + { url = "https://files.pythonhosted.org/packages/4b/43/c84a7900feced6a420e824ab61d316e4f59a6887d991bc143ecc294a09ed/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e", size = 1170406, upload-time = "2026-09-28T05:19:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/47/5d/6e2a32b9c3020af7a06b00fe446a2c5155ae76ac468c0b68d7188e524002/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95", size = 1300339, upload-time = "2026-09-28T05:20:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f9/78/19f3c9eb9a5c215b9fbde57be18ef7c1c78afa509b460d4e8f86026d741a/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa", size = 1336484, upload-time = "2026-09-28T05:19:02.841Z" }, + { url = "https://files.pythonhosted.org/packages/88/bb/d46c4519e989e022667705d8a56e83a09ad4ea50eae07ca4bb0f4708dd23/hypothesis-6.168.3-cp311-cp311-win_amd64.whl", hash = "sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462", size = 684646, upload-time = "2026-09-28T05:20:00.842Z" }, + { url = "https://files.pythonhosted.org/packages/7a/56/44a17266bcf1add5fba7c3df5c3920396a5e924d917fb0534a6f9e2b9b9a/hypothesis-6.168.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068", size = 793313, upload-time = "2026-09-28T05:20:22.045Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/1a25b11e9133cb54c02ee6cf99a61296ad461aabc7aae3399e1d0ff1f095/hypothesis-6.168.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96", size = 784855, upload-time = "2026-09-28T05:19:34.496Z" }, + { url = "https://files.pythonhosted.org/packages/8b/da/728d3210f0b0089337f37206a100d31ef546d1f99839e709f0a9ca443f8e/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed", size = 1123043, upload-time = "2026-09-28T05:19:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/53/86/bfb9ec344a8b3ef2fa6f59bb3d30c5b3a7d86d594107670539c34ee53d1b/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5", size = 1169121, upload-time = "2026-09-28T05:20:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/5a76dbafbcaf8fff075f09377cddefa17289ef05dbf51878dde88385b966/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018", size = 1298749, upload-time = "2026-09-28T05:19:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/28/15/0e65af96b353f2c4bdc283c53ed0980e5ac5009afe97ef3b23c77b364664/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837", size = 1335271, upload-time = "2026-09-28T05:20:54.005Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c5/37eab2155664aa544df321f6d3940b89c6702d79a72fdc98d7b5a2286f8c/hypothesis-6.168.3-cp312-cp312-win_amd64.whl", hash = "sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90", size = 682211, upload-time = "2026-09-28T05:18:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a2/6787da846d929e52fc3344d803299c45782dbeac287528af08380e984bf8/hypothesis-6.168.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7", size = 793254, upload-time = "2026-09-28T05:19:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/89/ba/2893f5ca42501f4d562cba3229c8994c3a3e5fac66ef60c1e0e0b5220a5d/hypothesis-6.168.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499", size = 784699, upload-time = "2026-09-28T05:18:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/7d7349daf75b71f6f35876f8de115e974c5c04d600e86df1b2779b0883ab/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89", size = 1123016, upload-time = "2026-09-28T05:20:36.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/7774e1ea072708ea46cb25c4aeee5f9978b6c271764809069e8a6789858e/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0", size = 1168989, upload-time = "2026-09-28T05:20:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7d/113992abed9efbd7944e3a496a382da58152dce126fddf6419ff31a0a57d/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a", size = 1298626, upload-time = "2026-09-28T05:19:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/d5/65/3659fa5e733027e5b37a27486e57f4053535e40853d23501c422bb275043/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248", size = 1335184, upload-time = "2026-09-28T05:20:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6c/35aab2221b5ea65125340f236e1a765a9e9f3b28d89a389ea0033fed29f7/hypothesis-6.168.3-cp313-cp313-win_amd64.whl", hash = "sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766", size = 682173, upload-time = "2026-09-28T05:19:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/27b0cb56ff5d2bc92458bf6fcebdb1f0dc01f57f043a178e9f9d74498169/hypothesis-6.168.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2", size = 793287, upload-time = "2026-09-28T05:20:38.522Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bd/5342f95c3bc36586777ef8cb8eba87ac7acf0184210327fb0e6d8654e295/hypothesis-6.168.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba", size = 784857, upload-time = "2026-09-28T05:20:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/fc774241518a0588d362680a3084275058aab86d6bc02a2c61e1073142d0/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897", size = 1123330, upload-time = "2026-09-28T05:19:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/131f16775a3dca5f4fe27f0d6ad9a9851600098a54a872d163f6ebf0b664/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b", size = 1169178, upload-time = "2026-09-28T05:19:53.436Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/c9f5bff8b73321c9fd12f2d69666c6e4861b97b60f5f24fdd6ac293007fd/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2", size = 1299127, upload-time = "2026-09-28T05:20:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/d4618f8ab12dd76c4d58ea052252759aa2bf0c4e71f4b502ba5d577e0b9f/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86", size = 1335390, upload-time = "2026-09-28T05:20:10.326Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/727161e17cc297df1aeea945de05c532033b66751625bec192bc5b5ff707/hypothesis-6.168.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa", size = 624291, upload-time = "2026-09-28T05:18:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/77/23/2f9b506392a0b8712440bcb781abc5713be9ffac1303b6d10f3669050882/hypothesis-6.168.3-cp314-cp314-win_amd64.whl", hash = "sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2", size = 682064, upload-time = "2026-09-28T05:20:16.336Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/efabfd95eb2b69c0c9fa1e1c83c8290aa2715d46baeb5a7764faf09c133a/hypothesis-6.168.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074", size = 791875, upload-time = "2026-09-28T05:19:37.789Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fc/2a0ada1623a9a33048bbf9f00a7c92513398c6b8865cba5efb854262ce30/hypothesis-6.168.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe", size = 783412, upload-time = "2026-09-28T05:18:48.266Z" }, + { url = "https://files.pythonhosted.org/packages/41/31/73c615d37eeb12da0209555612c32ced628d860267c23cfd2691c6207aea/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675", size = 1121662, upload-time = "2026-09-28T05:19:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/92/54/1893344a3b8bbf83fa7c9bb7e96f6836580213854f46671cb749f7b4706c/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4", size = 1167761, upload-time = "2026-09-28T05:18:49.85Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a61d82968febd25a0f08ffbbfabfaeead66f68fc5fa73f3c9ece1c36fc8f/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7", size = 1297309, upload-time = "2026-09-28T05:19:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/fe8e4cf6d39d02f4efadaa351427fa13722086cfc54a2da3579741c5177a/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96", size = 1334260, upload-time = "2026-09-28T05:19:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/39/6a/09177ebc62f4778f94379ecade6a51299d3d8c4f4b75ba636bf3a0202364/hypothesis-6.168.3-cp314-cp314t-win_amd64.whl", hash = "sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f", size = 681992, upload-time = "2026-09-28T05:19:48.043Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/890bf33d608cd63348d3146a3ca83362fbf09e589c5e5230a00c403070f4/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f", size = 791231, upload-time = "2026-09-28T05:18:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/03/dd/fc36b204f8aa7437c676576d9437f0422de46aa8a729e6cd5bbb0224e759/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b", size = 783171, upload-time = "2026-09-28T05:19:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d2/3cd087d577db67c404991d7723e64d7cd75570cdc82a0f7329aed6f2086c/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408", size = 1121152, upload-time = "2026-09-28T05:18:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/42/6f/a05a68cc66e7a22701c50bf94fb9ddeb36f4f5d0de3cc130066709eda621/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa", size = 1144069, upload-time = "2026-09-28T05:20:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/fdd7a093fb2fb3a0ce24256c92ad5bf67936f4669bd06d5ceae4298150ec/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128", size = 1146692, upload-time = "2026-09-28T05:20:25.83Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6dbd4377e935505ae4fc8ee4c7b18c69994c4bbaca015447c470218bdbee/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38", size = 1189480, upload-time = "2026-09-28T05:20:46.734Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bf/ff288b496b690000d2686dcfa7f67855e4c5a6dddb464ed8e4880be83bb2/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc", size = 1167109, upload-time = "2026-09-28T05:19:24.758Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d1/3fee2bc29fc747fabbb27e174515592e390809ca465d3bbbabbcfa3235a8/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6", size = 1126823, upload-time = "2026-09-28T05:18:52.636Z" }, + { url = "https://files.pythonhosted.org/packages/58/6a/585294392fa6a6d9719446a042a777ba98ecca265d9e48cbedc10c88df0f/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483", size = 1155773, upload-time = "2026-09-28T05:20:51.268Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/fa283ff79996debf1ff08593e01f8b773eb8211b19b3d6d5d491317a65bf/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09", size = 1296671, upload-time = "2026-09-28T05:18:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/98f9c2f628da5afabb6c3e8df5d2d299d2d50bf306b14a6e26d3954c4951/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580", size = 1422001, upload-time = "2026-09-28T05:19:04.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/f2109ade29a7ec284d55bca328d784743e8b13e321576fd34ee7e65f99af/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5", size = 1374182, upload-time = "2026-09-28T05:20:27.764Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/e05d5f72441564a3bebc71fa155deafd0fd3b015d6014ec8a00edf6e42bb/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62", size = 1278402, upload-time = "2026-09-28T05:20:56.332Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/6988a7f1f69c5c530687dce03a32c157e2d878e3a2e32af9269ce016b78a/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd", size = 1297770, upload-time = "2026-09-28T05:19:11.387Z" }, + { url = "https://files.pythonhosted.org/packages/a2/28/5bb82b60b836bd2329e1fe01ad94efc2b14dfa806f8e50ed14b795d78770/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16", size = 1333977, upload-time = "2026-09-28T05:20:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/d419841b8f65481ea1a50c4ba36f2670d48b8c7a51e4e8586089564cf7f6/hypothesis-6.168.3-cp315-abi3.abi3t-win32.whl", hash = "sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957", size = 675394, upload-time = "2026-09-28T05:19:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/1de6a2ad100e44817f2e9a8e8ba3eeaf4769621f4f87f2d4966c4971fcc4/hypothesis-6.168.3-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a", size = 681699, upload-time = "2026-09-28T05:19:09.957Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/be3370fd02734d6b1d950183ae9224580d19fa8d356b95348d60f1e3eeb7/hypothesis-6.168.3-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5", size = 679849, upload-time = "2026-09-28T05:19:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/27960db1d45a94497e1681fb9d7b9140b44da4e655ba20f0d82f95d10230/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575", size = 793135, upload-time = "2026-09-28T05:19:14.056Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cb/be08379286c1925fcd651b92ed835f470a83b859b5c484b3431da9d851f5/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca", size = 788995, upload-time = "2026-09-28T05:20:34.164Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/6d885d139b2ed76e2f4908cf10eae56acc73bf05cbef286357c4559c9a04/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696", size = 1124974, upload-time = "2026-09-28T05:19:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/05/cc/b302ae809b48254830cae57801d0af0b66783df515b5dd56b9f21c8dfa86/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d", size = 1171967, upload-time = "2026-09-28T05:20:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/51031080a6600f5b07adf5f9dfa117345c2adda07a5c24bda523e70bd1e9/hypothesis-6.168.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b", size = 685641, upload-time = "2026-09-28T05:19:44.609Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1661,6 +1749,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "hypothesis" }, { name = "pytest" }, { name = "ruff" }, ] @@ -1670,6 +1759,7 @@ policyengine = [ [package.dev-dependencies] dev = [ + { name = "hypothesis" }, { name = "pytest" }, ] @@ -1677,6 +1767,7 @@ dev = [ requires-dist = [ { name = "cryptography", specifier = ">=46.0.3" }, { name = "httpx", specifier = ">=0.27.0" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.100,<7" }, { name = "microplex", specifier = ">=0.1.0" }, { name = "numpy", specifier = ">=1.24.0" }, { name = "odfpy", specifier = ">=1.4.1" }, @@ -1697,7 +1788,10 @@ requires-dist = [ provides-extras = ["dev", "policyengine"] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.0.0,<9" }] +dev = [ + { name = "hypothesis", specifier = ">=6.100,<7" }, + { name = "pytest", specifier = ">=8.0.0,<9" }, +] [[package]] name = "policyengine-core"