From 127d8f6f09abd1c42fc86f80d179e828aed8413c Mon Sep 17 00:00:00 2001 From: Bradley Reynolds Date: Sat, 20 Jun 2026 22:21:51 +0000 Subject: [PATCH] CI: add linting and type checking Signed-off-by: Bradley Reynolds --- .github/dependabot.yaml | 26 ++ .github/workflows/python-ci.yaml | 48 +++ .github/workflows/stdlib-introspect.yaml | 6 + .pre-commit-config.yaml | 26 ++ noxfile.py | 42 +++ pyproject.toml | 46 +++ tools/coverage_diff.py | 152 +++++---- tools/merge_summary.py | 103 +++--- tools/stdlib_introspect.py | 192 ++++++++---- uv.lock | 383 +++++++++++++++++++++++ 10 files changed, 870 insertions(+), 154 deletions(-) create mode 100644 .github/workflows/python-ci.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index baded96..034156a 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -8,3 +8,29 @@ updates: ci-dependencies: patterns: - "*" + cooldown: + default-days: 7 + + - package-ecosystem: uv + directories: + - / + - packages/* + schedule: + interval: monthly + groups: + python-dependencies: + patterns: + - "*" + cooldown: + default-days: 7 + + - package-ecosystem: pre-commit + directory: / + schedule: + interval: monthly + groups: + pre-commit-dependencies: + patterns: + - "*" + cooldown: + default-days: 7 diff --git a/.github/workflows/python-ci.yaml b/.github/workflows/python-ci.yaml new file mode 100644 index 0000000..29053bc --- /dev/null +++ b/.github/workflows/python-ci.yaml @@ -0,0 +1,48 @@ +name: Python lint and test + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version-file: "pyproject.toml" + + - name: Sync dependencies + run: uv sync + + - name: Run prek + run: uv run prek run --all-files + + - name: Check formatting + run: uv run ruff format --check . + + - name: Run Ruff checks + run: uv run ruff check --output-format=github . + + - name: Run mypy + run: uv run mypy --strict src/ packages/ tests/ + + - name: Run ty + run: uv run ty check --output-format=github . diff --git a/.github/workflows/stdlib-introspect.yaml b/.github/workflows/stdlib-introspect.yaml index d1269fe..47b6381 100644 --- a/.github/workflows/stdlib-introspect.yaml +++ b/.github/workflows/stdlib-introspect.yaml @@ -41,6 +41,8 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: @@ -81,6 +83,8 @@ jobs: timeout-minutes: 1 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: @@ -113,6 +117,8 @@ jobs: TARGET_VERSION: ${{ github.event.inputs.target_version || '3.14' }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2746197 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +repos: + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes + + - repo: builtin + hooks: + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: trailing-whitespace + args: [ --markdown-linebreak-ext=md ] + - id: mixed-line-ending + args: [ --fix=lf ] + - id: end-of-file-fixer + + - repo: https://github.com/astral-sh/uv-pre-commit + rev: b17b79c1cab56c3d1025ad36cb117fe602e570fd # frozen: 0.11.23 + hooks: + - id: uv-lock + + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: 9257c6050c0261b8c57e712f632dc4a8010109a9 # frozen: v1.25.2 + hooks: + - id: zizmor diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..541686d --- /dev/null +++ b/noxfile.py @@ -0,0 +1,42 @@ +"""Noxfile.""" + +import shutil +from pathlib import Path + +import nox + +nox.options.default_venv_backend = "none" +nox.options.sessions = ["lints"] + + +CLEANABLE_TARGETS = [ + # Generated bytecode + "./**/__pycache__", + "./**/*.pyc", + "./**/*.pyo", + # Centralized tool cache + "./.cache", +] + + +@nox.session +def lints(session: nox.Session) -> None: + """Run lints.""" + session.run("prek", "run", "--all-files") + session.run("ruff", "format", ".") + session.run("ruff", "check", "--fix", ".") + session.run("mypy", "--strict", "src/", "packages/", "tests/") + session.run("ty", "check", ".") + + +@nox.session +def clean(_: nox.Session) -> None: + """Clean cache, .pyc, .pyo, and test/build artifact files from project.""" + count = 0 + for searchpath in CLEANABLE_TARGETS: + for filepath in Path().glob(searchpath): + if filepath.is_dir(): + shutil.rmtree(filepath) + else: + filepath.unlink() + count += 1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fce3860 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[project] +name = "cpython-docs-compendium" +version = "2026.06.20" +authors = [ + { name = "Bradley Reynolds", email = "bradley.reynolds@tailstory.dev" }, +] +requires-python = ">=3.14" + +[dependency-groups] +dev = [ + # DX + "nox>=2026.4.10", + "prek>=0.4.4", + # Linters + "ruff>=0.15.17", + "mypy>=2.1.0", + "ty>=0.0.49", +] + +[tool.uv] +# Sync with Dependabot's uv version +required-version = ">=0.11.8" +exclude-newer = "P7D" + +[tool.ruff] +line-length = 120 +cache-dir = ".cache/ruff" + +[tool.ruff.lint] +select = [ + "ALL", +] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.mypy] +pretty = true +num_workers = 4 +native_parser = true # required by num_workers +plugins = ["pydantic.mypy"] +cache_dir = ".cache/mypy" + +# Still need to tell Mypy to ignore these +[tool.ty.rules] +unused-ignore-comment = "ignore" diff --git a/tools/coverage_diff.py b/tools/coverage_diff.py index 92a4a48..07c610b 100644 --- a/tools/coverage_diff.py +++ b/tools/coverage_diff.py @@ -14,12 +14,14 @@ The inventory carries no version metadata, so it cannot say when an entity was added/removed -- those deltas come from the matrix (merge_summary.py), not here. """ + from __future__ import annotations + +import argparse +import json import os import re -import json import time -import argparse import urllib.error import urllib.request from collections import Counter @@ -42,8 +44,11 @@ "nt.": "os.", } MODULE_ALIASES = { - "posixpath": "os.path", "ntpath": "os.path", "genericpath": "os.path", - "posix": "os", "nt": "os", + "posixpath": "os.path", + "ntpath": "os.path", + "genericpath": "os.path", + "posix": "os", + "nt": "os", } CELL_VERSION = re.compile(r"-py([0-9][^-]*)$") @@ -54,24 +59,29 @@ def version_key(version): numbers = re.findall(r"\d+", version) return tuple(int(number) for number in numbers[:2]) if numbers else (0,) + def version_label(version): return ".".join(str(part) for part in version_key(version)) + def cell_version(cell): match = CELL_VERSION.search(cell) return version_label(match.group(1)) if match else None + def normalize(qualname): if qualname in MODULE_ALIASES: return MODULE_ALIASES[qualname] for prefix, replacement in PREFIX_ALIASES.items(): if qualname.startswith(prefix): - return replacement + qualname[len(prefix):] + return replacement + qualname[len(prefix) :] return qualname + def normalize_module(module): return MODULE_ALIASES.get(module, module) + def percent(part, whole): return round(100 * part / whole, 1) if whole else 0.0 @@ -93,11 +103,11 @@ def http_get(url, attempts=4): with urllib.request.urlopen(request, timeout=30) as response: return response.read() except urllib.error.HTTPError: - raise # 4xx/5xx: caller decides (404 -> dev fallback) + raise # 4xx/5xx: caller decides (404 -> dev fallback) except urllib.error.URLError: if attempt == attempts - 1: raise - time.sleep(2 ** attempt) + time.sleep(2**attempt) raise ValueError("attempts must be >= 1") @@ -115,7 +125,7 @@ def documented_names(version, inventory_dir=None): except urllib.error.HTTPError as error: if error.code != 404: raise - data = http_get(DEV_INVENTORY_URL) # in-dev minor with no numbered inventory yet + data = http_get(DEV_INVENTORY_URL) # in-dev minor with no numbered inventory yet used_dev = True inventory = soi.Inventory(zlib=data) return {obj.name for obj in inventory.objects if obj.domain == "py"}, used_dev @@ -171,14 +181,16 @@ def gap_records(surface, missing_from_official_docs): rows = [] for name in missing_from_official_docs: record = surface[name] - rows.append({ - "qualname": name, - "kind": record["kind"], - "module": normalize_module(record["module"]), - "signature": record.get("signature"), - "has_docstring": bool(record.get("doc_resolved")), - "is_data": record["kind"] == "data", - }) + rows.append( + { + "qualname": name, + "kind": record["kind"], + "module": normalize_module(record["module"]), + "signature": record.get("signature"), + "has_docstring": bool(record.get("doc_resolved")), + "is_data": record["kind"] == "data", + }, + ) rows.sort(key=lambda row: (row["module"], row["qualname"])) return rows @@ -188,23 +200,27 @@ def default_target(versions): def main(): - parser = argparse.ArgumentParser(description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("union", help="stdlib_api_union.jsonl from the aggregate job") - parser.add_argument("--target-version", help="minor to build the docs.python.org gap for " - "(default: latest stable = second-highest in the matrix)") + parser.add_argument( + "--target-version", + help="minor to build the docs.python.org gap for (default: latest stable = second-highest in the matrix)", + ) parser.add_argument("-o", "--output", default="official_docs_coverage_by_version.json") parser.add_argument("--gap-out", metavar="PATH", help="default: official_docs_gap_.jsonl") - parser.add_argument("--inventory-dir", metavar="DIR", - help="read .inv from here before fetching (offline/cached runs)") - parser.add_argument("--md-summary", metavar="PATH", - help="write the Markdown report here (defaults to $GITHUB_STEP_SUMMARY)") + parser.add_argument( + "--inventory-dir", metavar="DIR", help="read .inv from here before fetching (offline/cached runs)", + ) + parser.add_argument( + "--md-summary", metavar="PATH", help="write the Markdown report here (defaults to $GITHUB_STEP_SUMMARY)", + ) args = parser.parse_args() union = load_union(args.union) - versions = sorted({cell_version(cell) for record in union - for cell in record.get("cells", []) if cell_version(cell)}, - key=version_key) + versions = sorted( + {cell_version(cell) for record in union for cell in record.get("cells", []) if cell_version(cell)}, + key=version_key, + ) results, surfaces, gaps, docs_onlys = {}, {}, {}, {} for version in versions: @@ -212,9 +228,11 @@ def main(): results[version] = summary surfaces[version], gaps[version], docs_onlys[version] = surface, missing, docs_only flag = " [dev fallback]" if summary["used_dev_fallback"] else "" - print(f" {version:8s} surface={summary['surface']:6d} covered={summary['covered']:6d} " - f"missing={summary['backlog']:6d} docs-only={summary['docs_only']:6d} " - f"({summary['coverage_pct']}%)" + flag) + print( + f" {version:8s} surface={summary['surface']:6d} covered={summary['covered']:6d} " + f"missing={summary['backlog']:6d} docs-only={summary['docs_only']:6d} " + f"({summary['coverage_pct']}%)" + flag, + ) target = args.target_version or (default_target(versions) if versions else None) @@ -226,58 +244,78 @@ def main(): gap_path = args.gap_out or f"official_docs_gap_{target}.jsonl" rows = gap_records(surfaces.get(target, {}), gaps.get(target, set())) with open(gap_path, "w", encoding="utf-8", newline="\n") as out_file: - for row in rows: - out_file.write(json.dumps(row) + "\n") + out_file.writelines(json.dumps(row) + "\n" for row in rows) - report(versions, results, target, rows, docs_onlys.get(target, set()), - args.output, gap_path, args) + report(versions, results, target, rows, docs_onlys.get(target, set()), args.output, gap_path, args) def report(versions, results, target, gap_rows, docs_only, output_path, gap_path, args): data_entries = sum(1 for row in gap_rows if row["is_data"]) lines = ["# docs.python.org coverage — stdlib API missing from the official reference", ""] if not versions: - no_versions = ("> **No versions found in the union.** The aggregate step produced " - "no cells, or the union schema is missing `cells`.") + no_versions = ( + "> **No versions found in the union.** The aggregate step produced " + "no cells, or the union schema is missing `cells`." + ) lines += [no_versions, ""] else: - intro = ("Each `undocumented` count is the introspected stdlib surface minus " - "docs.python.org's `py` inventory, per minor (OS-unioned) — the API the " - "official CPython reference does not document. The inventory has no version " - "metadata; added/removed deltas come from the matrix, not from here.") - lines += [f"Target version (gap): **{target}**.", "", intro, "", - "## Coverage by version", "", - "| version | surface | covered | undocumented | docs-only | coverage |", - "| --- | ---: | ---: | ---: | ---: | ---: |"] + intro = ( + "Each `undocumented` count is the introspected stdlib surface minus " + "docs.python.org's `py` inventory, per minor (OS-unioned) — the API the " + "official CPython reference does not document. The inventory has no version " + "metadata; added/removed deltas come from the matrix, not from here." + ) + lines += [ + f"Target version (gap): **{target}**.", + "", + intro, + "", + "## Coverage by version", + "", + "| version | surface | covered | undocumented | docs-only | coverage |", + "| --- | ---: | ---: | ---: | ---: | ---: |", + ] for version in versions: stats = results[version] marks = " ⭐" if version == target else (" (dev)" if stats["used_dev_fallback"] else "") - lines.append(f"| {version}{marks} | {stats['surface']} | {stats['covered']} " - f"| {stats['backlog']} | {stats['docs_only']} | {stats['coverage_pct']}% |") + lines.append( + f"| {version}{marks} | {stats['surface']} | {stats['covered']} " + f"| {stats['backlog']} | {stats['docs_only']} | {stats['coverage_pct']}% |", + ) lines.append("") - lines += [f"## Missing from the official {target} docs — {len(gap_rows)} undocumented", "", - f"Reference-entry core (callables/classes/etc.): **{len(gap_rows) - data_entries}**; " - f"`data` entries: **{data_entries}**.", "", - "| kind | count |", "| --- | ---: |"] + lines += [ + f"## Missing from the official {target} docs — {len(gap_rows)} undocumented", + "", + f"Reference-entry core (callables/classes/etc.): **{len(gap_rows) - data_entries}**; " + f"`data` entries: **{data_entries}**.", + "", + "| kind | count |", + "| --- | ---: |", + ] for kind, count in Counter(row["kind"] for row in gap_rows).most_common(): lines.append(f"| {kind} | {count} |") lines.append("") module_stats = results.get(target, {}).get("by_module", {}) per_module = Counter(row["module"] for row in gap_rows) - lines += [f"## Top {TOP_MODULES} modules by undocumented count ({target})", "", - "| module | undocumented | surface | coverage |", "| --- | ---: | ---: | ---: |"] + lines += [ + f"## Top {TOP_MODULES} modules by undocumented count ({target})", + "", + "| module | undocumented | surface | coverage |", + "| --- | ---: | ---: | ---: |", + ] for module, count in per_module.most_common(TOP_MODULES): stats = module_stats.get(module, {}) - lines.append(f"| `{module}` | {count} | {stats.get('surface', '?')} " - f"| {stats.get('coverage_pct', '?')}% |") + lines.append(f"| `{module}` | {count} | {stats.get('surface', '?')} | {stats.get('coverage_pct', '?')}% |") lines.append("") if docs_only: - docs_only_note = ("In the official docs inventory but not introspected: removed/renamed " - "API docs.python.org still lists, or names this run failed to " - "enumerate (normalization QA signal).") + docs_only_note = ( + "In the official docs inventory but not introspected: removed/renamed " + "API docs.python.org still lists, or names this run failed to " + "enumerate (normalization QA signal)." + ) lines += [f"## Docs-only — {target} ({len(docs_only)})", "", docs_only_note, ""] markdown_path = args.md_summary or os.environ.get("GITHUB_STEP_SUMMARY") diff --git a/tools/merge_summary.py b/tools/merge_summary.py index c5ecbc4..b77bef6 100644 --- a/tools/merge_summary.py +++ b/tools/merge_summary.py @@ -21,13 +21,15 @@ Stdlib only, no third-party deps. All file I/O is UTF-8. python merge_summary.py CELLS_DIR [-o stdlib_api_union.jsonl] [--md-summary PATH] """ + from __future__ import annotations -import sys + +import argparse +import glob +import json import os import re -import json -import glob -import argparse +import sys from collections import defaultdict # Filenames look like stdlib_api_ubuntu-latest_py3.14.jsonl. The os has no "_py" and @@ -46,20 +48,24 @@ def os_family(label): return "windows" return lowered + def version_key(version): """'3.14' -> (3, 14); tolerant of '3.15.0a1' and junk.""" numbers = re.findall(r"\d+", version) return tuple(int(number) for number in numbers[:2]) if numbers else (0,) + def format_version(version_tuple): return ".".join(str(part) for part in version_tuple) + def version_span(present_keys, matrix_keys): """(added_in, removed_in) for one entity from its OS-collapsed version set. added_in is floored at the matrix minimum: present at the floor means it was added then or in some earlier, unobserved release, recorded as '<=X.Y'. - removed_in is precise -- the first matrix minor where it disappears.""" + removed_in is precise -- the first matrix minor where it disappears. + """ floor = matrix_keys[0] added_in = "<=" + format_version(floor) if floor in present_keys else format_version(min(present_keys)) removed_in = None @@ -78,7 +84,7 @@ def __init__(self, path, os_label, version): self.version = version self.version_key = version_key(version) self.cell_id = f"{self.family}-py{version}" - self.records = {} # qualname -> record (last wins within a cell) + self.records = {} # qualname -> record (last wins within a cell) self.malformed = 0 def load(self): @@ -90,8 +96,8 @@ def load(self): try: record = json.loads(line) self.records[record["qualname"]] = record - except (json.JSONDecodeError, KeyError, TypeError): - self.malformed += 1 # tolerate a half-written dump from a crashed cell + except json.JSONDecodeError, KeyError, TypeError: + self.malformed += 1 # tolerate a half-written dump from a crashed cell def discover(cells_dir): @@ -108,9 +114,9 @@ def discover(cells_dir): def aggregate(cells): - present_cells = defaultdict(set) # qualname -> {cell_id} - present_families = defaultdict(set) # qualname -> {family} - present_version_keys = defaultdict(set) # qualname -> {version_key} + present_cells = defaultdict(set) # qualname -> {cell_id} + present_families = defaultdict(set) # qualname -> {family} + present_version_keys = defaultdict(set) # qualname -> {version_key} for cell in cells: for qualname in cell.records: present_cells[qualname].add(cell.cell_id) @@ -137,12 +143,12 @@ def aggregate(cells): def main(): - parser = argparse.ArgumentParser(description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("cells_dir", help="directory of stdlib_api__py.jsonl files") parser.add_argument("-o", "--output", default="stdlib_api_union.jsonl") - parser.add_argument("--md-summary", metavar="PATH", - help="write the Markdown report here (defaults to $GITHUB_STEP_SUMMARY)") + parser.add_argument( + "--md-summary", metavar="PATH", help="write the Markdown report here (defaults to $GITHUB_STEP_SUMMARY)", + ) args = parser.parse_args() cells = discover(args.cells_dir) @@ -151,8 +157,7 @@ def main(): # Always write the union file (even empty) so the upload step has an artifact; # newline="\n" so the artifact is byte-identical regardless of which runner ran us. with open(args.output, "w", encoding="utf-8", newline="\n") as out_file: - for qualname in union: - out_file.write(json.dumps(union_records[qualname]) + "\n") + out_file.writelines(json.dumps(union_records[qualname]) + "\n" for qualname in union) # Platform-exclusive: present on exactly one OS family across the matrix. families = sorted({cell.family for cell in cells}, key=lambda family: FAMILY_ORDER.get(family, 99)) @@ -166,8 +171,16 @@ def main(): version_keys = sorted({cell.version_key for cell in cells}) transitions = [] for earlier, later in zip(version_keys, version_keys[1:]): - added = [qualname for qualname in union if later in present_version_keys[qualname] and earlier not in present_version_keys[qualname]] - removed = [qualname for qualname in union if earlier in present_version_keys[qualname] and later not in present_version_keys[qualname]] + added = [ + qualname + for qualname in union + if later in present_version_keys[qualname] and earlier not in present_version_keys[qualname] + ] + removed = [ + qualname + for qualname in union + if earlier in present_version_keys[qualname] and later not in present_version_keys[qualname] + ] transitions.append((earlier, later, added, removed)) report(cells, union, exclusive, families, transitions, version_keys, args) @@ -178,39 +191,59 @@ def report(cells, union, exclusive, families, transitions, version_keys, args): lines = ["# stdlib introspection — cross-platform union", ""] if not cells: - lines += ["> **No cell artifacts were found.** Every matrix cell failed to produce a dump, or the download step pulled nothing.", ""] + lines += [ + "> **No cell artifacts were found.** Every matrix cell failed to produce a dump, or the download step pulled nothing.", + "", + ] else: versions = sorted({format_version(cell.version_key) for cell in cells}, key=version_key) - lines += [f"Aggregated **{len(cells)}** cells — families: {', '.join(families)}; Python: {', '.join(versions)}.", "", - f"**Union surface: {len(union)} unique qualnames.**", ""] - - lines += ["## Per-cell entity counts", "", - "| cell | os | python | entities |", "| --- | --- | --- | ---: |"] + lines += [ + f"Aggregated **{len(cells)}** cells — families: {', '.join(families)}; Python: {', '.join(versions)}.", + "", + f"**Union surface: {len(union)} unique qualnames.**", + "", + ] + + lines += ["## Per-cell entity counts", "", "| cell | os | python | entities |", "| --- | --- | --- | ---: |"] for cell in rows: note = f" ⚠️ {cell.malformed} bad lines" if cell.malformed else "" lines.append(f"| `{cell.cell_id}` | {cell.os_label} | {cell.version} | {len(cell.records)}{note} |") lines.append("") - lines += ["## Platform-exclusive APIs", "", - "Qualnames that appear on exactly one OS family across the whole matrix.", "", - "| platform | exclusive qualnames |", "| --- | ---: |"] + lines += [ + "## Platform-exclusive APIs", + "", + "Qualnames that appear on exactly one OS family across the whole matrix.", + "", + "| platform | exclusive qualnames |", + "| --- | ---: |", + ] for family in families: lines.append(f"| {family} | {len(exclusive.get(family, []))} |") lines.append("") if transitions: floor_label = format_version(version_keys[0]) - lines += ["## Per-version deltas (OS-collapsed, adjacent minors)", "", - f"An entity is present in a minor if it appears in **any** OS cell for it. " - f"`added_in` for entities already present in {floor_label} is recorded as " - f"`<={floor_label}` — the matrix floor bounds it.", "", - "| transition | added | removed |", "| --- | ---: | ---: |"] + lines += [ + "## Per-version deltas (OS-collapsed, adjacent minors)", + "", + f"An entity is present in a minor if it appears in **any** OS cell for it. " + f"`added_in` for entities already present in {floor_label} is recorded as " + f"`<={floor_label}` — the matrix floor bounds it.", + "", + "| transition | added | removed |", + "| --- | ---: | ---: |", + ] for earlier, later, added, removed in transitions: lines.append(f"| {format_version(earlier)} → {format_version(later)} | {len(added)} | {len(removed)} |") lines.append("") else: - lines += ["## Per-version deltas", "", - "_Need at least two Python minors in the matrix to compute deltas._", ""] + lines += [ + "## Per-version deltas", + "", + "_Need at least two Python minors in the matrix to compute deltas._", + "", + ] markdown_path = args.md_summary or os.environ.get("GITHUB_STEP_SUMMARY") if markdown_path: diff --git a/tools/stdlib_introspect.py b/tools/stdlib_introspect.py index c70655a..66ebecf 100644 --- a/tools/stdlib_introspect.py +++ b/tools/stdlib_introspect.py @@ -16,33 +16,42 @@ Per-type dunders are excluded by default (the docs cover them in the data model section); --include-dunders keeps them, flagged with is_dunder=True. """ + from __future__ import annotations -import sys -import os + +import argparse +import contextlib +import importlib +import inspect import io import json -import inspect +import os import pkgutil -import importlib -import warnings -import argparse -import contextlib import platform +import sys +import warnings # Modules with import-time side effects (browser/print) or that we never document. SKIP_MODULES = { - "antigravity", # opens a web browser on import - "this", # prints the Zen of Python - "__hello__", "__phello__", # print on import - "test", # CPython's own test suite - "idlelib", # the IDLE GUI app - "turtledemo", # demo scripts - "lib2to3", # grammar/test heavy; removed in 3.13 + "antigravity", # opens a web browser on import + "this", # prints the Zen of Python + "__hello__", + "__phello__", # print on import + "test", # CPython's own test suite + "idlelib", # the IDLE GUI app + "turtledemo", # demo scripts + "lib2to3", # grammar/test heavy; removed in 3.13 } TEST_PARTS = {"test", "tests"} -def is_dunder(name): return len(name) > 4 and name.startswith("__") and name.endswith("__") -def is_private(name): return name.startswith("_") and not is_dunder(name) + +def is_dunder(name): + return len(name) > 4 and name.startswith("__") and name.endswith("__") + + +def is_private(name): + return name.startswith("_") and not is_dunder(name) + @contextlib.contextmanager def _silenced(): @@ -53,6 +62,7 @@ def _silenced(): with contextlib.redirect_stdout(sink), contextlib.redirect_stderr(sink): yield + def safe_import(name): short_name = name.split(".")[-1] if name in SKIP_MODULES or short_name in SKIP_MODULES: @@ -63,9 +73,10 @@ def safe_import(name): return importlib.import_module(name) except KeyboardInterrupt: raise - except BaseException: # ImportError, platform gates, missing C libs, etc. + except BaseException: # ImportError, platform gates, missing C libs, etc. return None + def roster(include_private): names = getattr(sys, "stdlib_module_names", None) if not names: @@ -74,6 +85,7 @@ def roster(include_private): for top_level in sorted(name for name in names if include_private or not name.startswith("_")): yield from _emit(top_level, include_private, seen) + def _emit(name, include_private, seen): if name in seen: return @@ -94,15 +106,19 @@ def _emit(name, include_private, seen): # __main__ submodules are `python -m pkg` entry points, not API surface, and # importing them RUNS code (tkinter opens a Tk mainloop, asyncio starts a stdin # REPL). Never import them -- nor any other dunder-named submodule. - if (is_dunder(submodule_short) - or (is_private(submodule_short) and not include_private) - or submodule_short in TEST_PARTS): + if ( + is_dunder(submodule_short) + or (is_private(submodule_short) and not include_private) + or submodule_short in TEST_PARTS + ): continue yield from _emit(submodule, include_private, seen) -SEEN = {} # id(obj) -> canonical qualname (first sighting) + +SEEN = {} # id(obj) -> canonical qualname (first sighting) RECORDS = {} -PENDING = {} # id(obj) -> [alias qualnames seen before the canonical one] +PENDING = {} # id(obj) -> [alias qualnames seen before the canonical one] + def kind_of(entity, in_class): if inspect.ismodule(entity): @@ -113,17 +129,19 @@ def kind_of(entity, in_class): return "property" if inspect.isgetsetdescriptor(entity) or inspect.ismemberdescriptor(entity): return "descriptor" - if inspect.isroutine(entity): # function / builtin / method / method_descriptor + if inspect.isroutine(entity): # function / builtin / method / method_descriptor return "method" if in_class else "function" return "data" + def get_signature(entity): try: return str(inspect.signature(entity)), "inspect" - except (ValueError, TypeError): + except ValueError, TypeError: text_signature = getattr(entity, "__text_signature__", None) return (text_signature, "text_signature") if text_signature else (None, "none") + def doc_info(entity): has_own_doc = bool(getattr(entity, "__doc__", None)) resolved_doc = inspect.getdoc(entity) @@ -132,11 +150,13 @@ def doc_info(entity): first_line = resolved_doc.strip().splitlines()[0][:100] return has_own_doc, bool(resolved_doc), first_line + def attach(canonical, alias): existing = RECORDS.get(canonical) if existing is not None and alias != canonical and alias not in existing["aliases"]: existing["aliases"].append(alias) + def note_alias(entity, qualname): try: object_id = id(entity) @@ -147,17 +167,27 @@ def note_alias(entity, qualname): else: PENDING.setdefault(object_id, []).append(qualname) + def record(qualname, kind, module, parent, entity, short_name): - signature, signature_source = get_signature(entity) if kind in {"function", "method", "class", "exception"} else (None, "n/a") + signature, signature_source = ( + get_signature(entity) if kind in {"function", "method", "class", "exception"} else (None, "n/a") + ) has_own_doc, has_resolved_doc, first_line = doc_info(entity) RECORDS[qualname] = { - "qualname": qualname, "kind": kind, "module": module, "parent": parent, + "qualname": qualname, + "kind": kind, + "module": module, + "parent": parent, "is_dunder": is_dunder(short_name), - "signature": signature, "sig_source": signature_source, - "doc_own": has_own_doc, "doc_resolved": has_resolved_doc, "doc_firstline": first_line, + "signature": signature, + "sig_source": signature_source, + "doc_own": has_own_doc, + "doc_resolved": has_resolved_doc, + "doc_firstline": first_line, "aliases": [], } + def process(entity, qualname, parent, module_name, short_name, in_class, include_dunders, include_private): kind = kind_of(entity, in_class) if kind == "data": @@ -168,7 +198,7 @@ def process(entity, qualname, parent, module_name, short_name, in_class, include return object_id = id(entity) if object_id in SEEN: - attach(SEEN[object_id], qualname) # re-export under another name + attach(SEEN[object_id], qualname) # re-export under another name return SEEN[object_id] = qualname kind = kind_of(entity, in_class) @@ -180,7 +210,17 @@ def process(entity, qualname, parent, module_name, short_name, in_class, include for child_name, child_value in sorted(vars(entity).items()): if (is_dunder(child_name) and not include_dunders) or (is_private(child_name) and not include_private): continue - process(child_value, f"{qualname}.{child_name}", qualname, module_name, child_name, True, include_dunders, include_private) + process( + child_value, + f"{qualname}.{child_name}", + qualname, + module_name, + child_name, + True, + include_dunders, + include_private, + ) + def walk_module(module, include_dunders, include_private): module_name = module.__name__ @@ -188,25 +228,32 @@ def walk_module(module, include_dunders, include_private): for name, value in sorted(vars(module).items()): if (is_dunder(name) and not include_dunders) or (is_private(name) and not include_private): continue - if inspect.ismodule(value): # imported module ref (ast.sys, os.path); roster handles real modules + if inspect.ismodule(value): # imported module ref (ast.sys, os.path); roster handles real modules continue owner = getattr(value, "__module__", None) if owner is not None and owner != module_name: - note_alias(value, f"{module_name}.{name}") # imported from elsewhere + note_alias(value, f"{module_name}.{name}") # imported from elsewhere continue process(value, f"{module_name}.{name}", module_name, module_name, name, False, include_dunders, include_private) + def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("-o", "--output", default="stdlib_api.jsonl") parser.add_argument("--include-dunders", action="store_true") parser.add_argument("--include-private", action="store_true") - parser.add_argument("--md-summary", metavar="PATH", - help="also write the summary as Markdown here " - "(defaults to $GITHUB_STEP_SUMMARY when that is set)") - parser.add_argument("--min-entities", type=int, default=5000, - help="sanity gate: exit non-zero if fewer than N records were " - "produced (a near-empty dump means the build is broken)") + parser.add_argument( + "--md-summary", + metavar="PATH", + help="also write the summary as Markdown here (defaults to $GITHUB_STEP_SUMMARY when that is set)", + ) + parser.add_argument( + "--min-entities", + type=int, + default=5000, + help="sanity gate: exit non-zero if fewer than N records were " + "produced (a near-empty dump means the build is broken)", + ) args = parser.parse_args() scanned, failed = 0, [] @@ -217,10 +264,16 @@ def main(): continue has_own_doc, has_resolved_doc, first_line = doc_info(module) RECORDS[name] = { - "qualname": name, "kind": "module", "module": name, - "parent": name.rpartition(".")[0] or None, "is_dunder": False, - "signature": None, "sig_source": "n/a", - "doc_own": has_own_doc, "doc_resolved": has_resolved_doc, "doc_firstline": first_line, + "qualname": name, + "kind": "module", + "module": name, + "parent": name.rpartition(".")[0] or None, + "is_dunder": False, + "signature": None, + "sig_source": "n/a", + "doc_own": has_own_doc, + "doc_resolved": has_resolved_doc, + "doc_firstline": first_line, "aliases": [], } try: @@ -232,8 +285,7 @@ def main(): # Default encoding is cp1252 on Windows (crashes on non-ASCII docstrings) and text # mode there translates \n -> \r\n; pin UTF-8 + LF so every cell emits identical bytes. with open(args.output, "w", encoding="utf-8", newline="\n") as out_file: - for entry in records: - out_file.write(json.dumps(entry) + "\n") + out_file.writelines(json.dumps(entry) + "\n" for entry in records) stats = _stats(records, scanned, failed) _text_summary(stats, args.output) @@ -244,11 +296,15 @@ def main(): # Gate last, after the output and summaries are written, so a broken cell still # uploads its dump and renders a summary before the non-zero exit fails the job. if len(records) < args.min_entities: - sys.exit(f"\nSANITY GATE: only {len(records)} records (< --min-entities " - f"{args.min_entities}); this build looks broken.") + sys.exit( + f"\nSANITY GATE: only {len(records)} records (< --min-entities " + f"{args.min_entities}); this build looks broken.", + ) + def _stats(records, scanned, failed): from collections import Counter + callables = [entry for entry in records if entry["kind"] in {"function", "method"}] return { "total_records": len(records), @@ -261,29 +317,36 @@ def _stats(records, scanned, failed): "by_module": Counter(entry["qualname"].split(".")[0] for entry in records), } + def _percent(part, whole): - return f"{100*part//whole}%" if whole else "n/a" + return f"{100 * part // whole}%" if whole else "n/a" + def _text_summary(stats, output_path): print("\n=== stdlib introspection summary =========================") print(f"Python {sys.version.split()[0]} on {sys.platform}") print(f"modules scanned : {stats['scanned']} ({len(stats['failed'])} not introspectable here)") print(f"total entities : {stats['total_records']}") - print(" by kind : " + ", ".join(f"{kind}={count}" for kind, count in stats['kinds'].most_common())) - if stats['total_callables']: - print(f"callables w/ signature : {stats['with_signature']}/{stats['total_callables']} ({_percent(stats['with_signature'], stats['total_callables'])})") - if stats['total_records']: - print(f"entities w/ docstring : {stats['with_docstring']}/{stats['total_records']} ({_percent(stats['with_docstring'], stats['total_records'])})") + print(" by kind : " + ", ".join(f"{kind}={count}" for kind, count in stats["kinds"].most_common())) + if stats["total_callables"]: + print( + f"callables w/ signature : {stats['with_signature']}/{stats['total_callables']} ({_percent(stats['with_signature'], stats['total_callables'])})", + ) + if stats["total_records"]: + print( + f"entities w/ docstring : {stats['with_docstring']}/{stats['total_records']} ({_percent(stats['with_docstring'], stats['total_records'])})", + ) print("\ntop 12 modules by entity count:") - for module_name, count in stats['by_module'].most_common(12): + for module_name, count in stats["by_module"].most_common(12): print(f" {count:5d} {module_name}") - if stats['failed']: - shown = ", ".join(stats['failed'][:18]) - more = f" (+{len(stats['failed'])-18} more)" if len(stats['failed']) > 18 else "" + if stats["failed"]: + shown = ", ".join(stats["failed"][:18]) + more = f" (+{len(stats['failed']) - 18} more)" if len(stats["failed"]) > 18 else "" print(f"\nnot introspectable on this build ({len(stats['failed'])}): {shown}{more}") print(f"\nwrote {stats['total_records']} records -> {output_path}") print("=" * 58) + def _markdown_summary(stats, output_path, summary_path): python_version = sys.version.split()[0] lines = [ @@ -296,17 +359,21 @@ def _markdown_summary(stats, output_path, summary_path): f"| modules not introspectable | {len(stats['failed'])} |", f"| total entities | {stats['total_records']} |", ] - if stats['total_callables']: - lines.append(f"| callables w/ signature | {stats['with_signature']}/{stats['total_callables']} ({_percent(stats['with_signature'], stats['total_callables'])}) |") - lines.append(f"| entities w/ docstring | {stats['with_docstring']}/{stats['total_records']} ({_percent(stats['with_docstring'], stats['total_records'])}) |") + if stats["total_callables"]: + lines.append( + f"| callables w/ signature | {stats['with_signature']}/{stats['total_callables']} ({_percent(stats['with_signature'], stats['total_callables'])}) |", + ) + lines.append( + f"| entities w/ docstring | {stats['with_docstring']}/{stats['total_records']} ({_percent(stats['with_docstring'], stats['total_records'])}) |", + ) lines += ["", "### Entities by kind", "", "| kind | count |", "| --- | --- |"] - lines += [f"| {kind} | {count} |" for kind, count in stats['kinds'].most_common()] + lines += [f"| {kind} | {count} |" for kind, count in stats["kinds"].most_common()] lines += ["", "### Top modules by entity count", "", "| module | entities |", "| --- | --- |"] - lines += [f"| `{module_name}` | {count} |" for module_name, count in stats['by_module'].most_common(12)] + lines += [f"| `{module_name}` | {count} |" for module_name, count in stats["by_module"].most_common(12)] - failed = stats['failed'] + failed = stats["failed"] lines += ["", f"### Not introspectable on this build ({len(failed)})", ""] lines.append(", ".join(f"`{module_name}`" for module_name in sorted(failed)) if failed else "_none_") lines.append("") @@ -316,5 +383,6 @@ def _markdown_summary(stats, output_path, summary_path): with open(summary_path, "a", encoding="utf-8", newline="\n") as summary_file: summary_file.write("\n".join(lines) + "\n") + if __name__ == "__main__": main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..98ea7f7 --- /dev/null +++ b/uv.lock @@ -0,0 +1,383 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +] + +[[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 = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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 = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + +[[package]] +name = "cpython-docs-compendium" +version = "2026.6.20" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "nox" }, + { name = "prek" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=2.1.0" }, + { name = "nox", specifier = ">=2026.4.10" }, + { name = "prek", specifier = ">=0.4.4" }, + { name = "ruff", specifier = ">=0.15.17" }, + { name = "ty", specifier = ">=0.0.49" }, +] + +[[package]] +name = "dependency-groups" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/55/f054de99871e7beb81935dea8a10b90cd5ce42122b1c3081d5282fdb3621/dependency_groups-1.3.1.tar.gz", hash = "sha256:78078301090517fd938c19f64a53ce98c32834dfe0dee6b88004a569a6adfefd", size = 10093, upload-time = "2025-05-02T00:34:29.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/c7/d1ec24fb280caa5a79b6b950db565dab30210a66259d17d5bb2b3a9f878d/dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030", size = 8664, upload-time = "2025-05-02T00:34:27.085Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, +] + +[[package]] +name = "humanize" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, +] + +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nox" +version = "2026.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "attrs" }, + { name = "colorlog" }, + { name = "dependency-groups" }, + { name = "humanize" }, + { name = "packaging" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/e672c862a43cfca704d32359221fa3780226daa1e5db5dfc401bcc8be9c9/nox-2026.4.10.tar.gz", hash = "sha256:2d0af5374f3f37a295428c927d1b04a8182aa01762897d172446dda2f1ce9692", size = 4034839, upload-time = "2026-04-10T17:42:42.209Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/95/4df134a100b5a9a12378d5301b934366686ef6fbdaffcd21211d5654970e/nox-2026.4.10-py3-none-any.whl", hash = "sha256:082c117627590d9b90aa21f86df89b310b07c5842539524203bcb3c719f116c1", size = 75536, upload-time = "2026-04-10T17:42:40.664Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "prek" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/13/3d71b3adbf385f7dc7fb6e16d6e25421fd8398b45d8f8410a328bf22bd3f/prek-0.4.4.tar.gz", hash = "sha256:4ec5771153d158a0e4473933b7fd9b51e1b1f57f2df50aeb7560ea6812226dc5", size = 470641, upload-time = "2026-06-04T07:26:07.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/9f/68a577888edf7f2647a652b02899508ccd84e57ce1f79c51a44edfd308d7/prek-0.4.4-py3-none-linux_armv6l.whl", hash = "sha256:23cfd96a25de1c93e3c43c746643b80489e3b2fa49ca9c0ffd6022e51535c900", size = 5550271, upload-time = "2026-06-04T07:26:17.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/5427a0023116343a8d787b446536a7fddfa5db7eec7713dd05618da2bdfe/prek-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a427b792c4436f49732b1f6ebccf221fdcc6390c148474280da9c2c6eaabc9c4", size = 5910136, upload-time = "2026-06-04T07:26:20.616Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4f/d751e90b7e768e472e054cd41cbe502589436ca9c1a13bfe4fa9513f9cde/prek-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b998038fc92c990e03147eb5b95b0f2c394517f8857ab911aac8e092f1b9b3ab", size = 5470124, upload-time = "2026-06-04T07:26:29.23Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fe/e73241c5777b6f9b6b95132febbd27f9be9e89912e9e93c0982680593af2/prek-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9cebca8c15da4f1d6e3a25e6ae0611425c8596e926222050f2588c390e42df8a", size = 5732725, upload-time = "2026-06-04T07:26:19.133Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a3/329bd910e7e5d9d0eb5e571f3ba48023213744e78411afb81f5ef8356cab/prek-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8742ac26363e74c855df6215a709d5db183204d00ac0f1a722b13aed4da3cd0", size = 5457953, upload-time = "2026-06-04T07:26:23.41Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f8/d642990513d9707398506bad45d39173d84266231f7d919899f694aefe2c/prek-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2b7c8710546a1e894afa7ab022030cd4e21f1ee7ffb301b4360773d22f1f00f", size = 5860556, upload-time = "2026-06-04T07:26:11.692Z" }, + { url = "https://files.pythonhosted.org/packages/8d/17/a2e29cb278503a8c18612d8a62a15020648dc768e2e94bc4b4d4c9411e07/prek-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e02bd4d5e05c500e4d9f70f024e30d13aa361dc490724b7f476d2e35542c239f", size = 6652492, upload-time = "2026-06-04T07:26:09.962Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/ad3270b18135ee5d1af6f6cf4b0c8601b1cc2cb38d16e835081da820833a/prek-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f3a25041733de987a47e5a7bace47182a6f0e2ae5f960cb54c1d4630afd2591", size = 6113837, upload-time = "2026-06-04T07:26:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/d9/e8c201b9b41c4561673cea01c630ab604df89d13e952f87dbcb807d32588/prek-0.4.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0b04a0f36d07474f2a9fc5b1ba1197a1b326b2b211f39cd74cf0d4613545f7f4", size = 5729155, upload-time = "2026-06-04T07:26:26.194Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cd/227b0494fcbc91e8fe15c2a4db9e6dfff95314ef38db3e40e6ea96db249d/prek-0.4.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f032ccbe2d6edc345f81a6d772c18cc169d63c27b5a8292bfe416b352bdfee57", size = 5590775, upload-time = "2026-06-04T07:26:22.038Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e0/9d750b9cf21ece884afdc15668d0f005a36588fe1b0bb5ff4a8112ab51cb/prek-0.4.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bad3586fcea3e913f0edf2e8f132f97889e03976b7ee3d120fd294ad4e89a5eb", size = 5437864, upload-time = "2026-06-04T07:26:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/3a/48/e2e5c0299590ad18a253c0ac09508b615baa1b382010c511186572f711d3/prek-0.4.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4058638532c6dcbf0076d23b9264cbdd9f0f0e320762e237a6b9e4e4b854a766", size = 5718579, upload-time = "2026-06-04T07:26:27.786Z" }, + { url = "https://files.pythonhosted.org/packages/54/48/7fb3d4e7f664d1ce8ae35ec553872ffddf9fab4c5081735fdaae610b1e7c/prek-0.4.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:619bab14071670249777deea0cc0b29d904c4a514cf33b20e583900a544f0399", size = 6231622, upload-time = "2026-06-04T07:26:16.309Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/a4ddbf38034afe67cfa97c4bd81c86429ada098e7c323218d9f9fd061566/prek-0.4.4-py3-none-win32.whl", hash = "sha256:143154b329c05b2f9fa3230e604d02d9c4297dd43f96135a8ba166772e8ecd60", size = 5240317, upload-time = "2026-06-04T07:26:08.726Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8c/fe97b5b095187bb2f93bbe406bccf108c879e5e4c83f165809b0d16ce0fb/prek-0.4.4-py3-none-win_amd64.whl", hash = "sha256:c38c5140ae2ea55ebb02e6ca590a416664ea1af287cdd21f54daeec53a81015a", size = 5626104, upload-time = "2026-06-04T07:26:14.81Z" }, + { url = "https://files.pythonhosted.org/packages/25/63/3586226d536796e65f8e725b531d6104e55caaa18659bdcb512661629586/prek-0.4.4-py3-none-win_arm64.whl", hash = "sha256:3efa28fb37b9ddbafb7759da8d497f0d36cf02a05816e15d6541f5669d5d2114", size = 5470399, upload-time = "2026-06-04T07:26:13.231Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" }, + { url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" }, + { url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" }, + { url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" }, + { url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" }, + { url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" }, + { url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" }, +] + +[[package]] +name = "ty" +version = "0.0.49" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/8d/37cb91808069509d43a2a11743e12f1e854fd808dbef2203309d256718cd/ty-0.0.49.tar.gz", hash = "sha256:0a027bd0c9c75d035641a365d087ad883446057f9be0b9826251c2aecafbf145", size = 5884753, upload-time = "2026-06-12T03:08:20.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/de/9237c6a96356612dd0393db1e94cf21f903616adf3a3701bf3da6e4adc92/ty-0.0.49-py3-none-linux_armv6l.whl", hash = "sha256:12c0c4310b936d762a8586c210b53d4fa4bb361a04429afa89bf84b922e5e065", size = 11834671, upload-time = "2026-06-12T03:07:53.062Z" }, + { url = "https://files.pythonhosted.org/packages/8f/15/daf5a14a5e07012277d450c75325c94614e2acfec4c620c881486118c410/ty-0.0.49-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:737bfdc2caf9712a8580944dcdc80a450a37a4f2bc83c8fa9b7433b374f9e471", size = 11589570, upload-time = "2026-06-12T03:08:25.779Z" }, + { url = "https://files.pythonhosted.org/packages/7d/58/30bdf98436488aca25f0763bf7f92a061528d42461b686453029e845e4c5/ty-0.0.49-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab90c1baf3b1701d282fce4b02fa552a962d109f8972c46ef6b22429503bfea4", size = 10985236, upload-time = "2026-06-12T03:08:36.664Z" }, + { url = "https://files.pythonhosted.org/packages/22/45/ece503e4a1396e13a1a9a0cde51afe476a6506a1d557eeadf8ad45c83bc0/ty-0.0.49-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ce8ecf6ba6fc79bd137cc0557a754f7e5f2dfe9436412551d480d680e248ad", size = 11504302, upload-time = "2026-06-12T03:08:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/17/dc/5d09333d289dfbca1804eaade125c9e8a1a992a2a592a8b80c5e9b589ca9/ty-0.0.49-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10d85c6865c984e78661e0bd20b180514b4a289739224e84816e342bdf381e04", size = 11626629, upload-time = "2026-06-12T03:08:06.844Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/155f41c9dd7237c4b609211f29f77755a139ee6218605dadc7fe21d5e3c8/ty-0.0.49-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d96a67a206619e01fa92f35a22267ec634bba62be24b1d0e947020cc179995b", size = 12074481, upload-time = "2026-06-12T03:08:09.643Z" }, + { url = "https://files.pythonhosted.org/packages/96/4c/998ee13cd5045f1f8b36982de7343163832ac53f27debe01b0de0e8bd968/ty-0.0.49-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de9f648564e0a66344ef397770387cb0d093735f8679d2c5a08a4741e79814d", size = 12678042, upload-time = "2026-06-12T03:08:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/85/c9/9a505aba85c41ce54cbcaa14f8d79aa084b86151d2d70df11c4655b92898/ty-0.0.49-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5779179ab397d15f8c9dbb8f506ec1b1745f54eac639982f76ef3ce538943b50", size = 12316194, upload-time = "2026-06-12T03:08:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/ded37fb93503294abbc83c36470bb1413bea05048b745881d4470b518a06/ty-0.0.49-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792d4974e93cc09bd32f934586080bbbe21b8e777099cb521cb2de18b68a49f0", size = 12145507, upload-time = "2026-06-12T03:07:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/2f/07/392e80d78f02445f695b815bb9eb0fffacda68b03faee38c900f7b990815/ty-0.0.49-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:727bda86deb136073e525c2e78d60e38aedcce5d80579170844a52bbf7c1440d", size = 12365967, upload-time = "2026-06-12T03:08:12.553Z" }, + { url = "https://files.pythonhosted.org/packages/50/d3/31b0c2a7fbedd3373e389cb1d81b8d2128f6f868fafb46557736a6f9aca8/ty-0.0.49-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f2fc2bc4a8d2ff1cca59fd94772cabdfec4062d47a0b3a0784be46d94d0540b", size = 11475283, upload-time = "2026-06-12T03:08:28.334Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5b/329e101638920b468a3bb63059c9f66ef99b44aac501222c44832a507321/ty-0.0.49-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3724bd9badef333321578b6a941fbc571ebf49141ec2356a8590fbe4c9aa588d", size = 11645343, upload-time = "2026-06-12T03:08:15.246Z" }, + { url = "https://files.pythonhosted.org/packages/a9/76/c897e615e32f80ca81c8c1bc49b9a1f72ff9e3cfea0f8345ba505fe28472/ty-0.0.49-py3-none-musllinux_1_2_i686.whl", hash = "sha256:166c6eb52ee4af3c5a9bb267d165d93000daa55c6758cd8ff3199741fb75917d", size = 11725585, upload-time = "2026-06-12T03:08:33.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/e1/fdb42ee239f618800842681af5bb8598117e74512c10974a8b7b9086a898/ty-0.0.49-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:91e81d832c287b05782ee32eb1b801f62c1fa08df37d589d2b88c3f1d51c9731", size = 12237261, upload-time = "2026-06-12T03:08:31.105Z" }, + { url = "https://files.pythonhosted.org/packages/98/0f/a2d6a5fc9d0786cbeb3c200786da4e18c203589be3984bb5def83ca92320/ty-0.0.49-py3-none-win32.whl", hash = "sha256:7186af5ca9829d1f5d8916bcf767b8e819bfbf61b1b8ec843bb3fc699cb502e1", size = 11100789, upload-time = "2026-06-12T03:07:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9d/473ac8bc57b5a2d121da893bf9dd74a118efb19a01d711df1a6e397f05cc/ty-0.0.49-py3-none-win_amd64.whl", hash = "sha256:ae2142fc126a01effcca0c222908b0e6654b5ba1266d4e4d406e4866aef8e1d1", size = 12204644, upload-time = "2026-06-12T03:08:04.327Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/8959249da951ba3977fee20e688d28678b8a1d30a9ed4464228a85d45853/ty-0.0.49-py3-none-win_arm64.whl", hash = "sha256:75d5e2e7649765f31f4bed6c8adb149a75b18edd3fa6336dac4d0efc1a66466f", size = 11558965, upload-time = "2026-06-12T03:08:23.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/0e/933bacb37b57ae7928b0030eef205a3dbb3e37afdbdde5be2e113318958f/virtualenv-21.5.0.tar.gz", hash = "sha256:98847aadf5e2037e0e4d2e19528eb3aca6f23906422e59a510bff231a6d32fce", size = 4577424, upload-time = "2026-06-13T20:36:45.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/87/b0667ede418386ab631e48924b845d326f366d61e6bd08fe68a748fae4d4/virtualenv-21.5.0-py3-none-any.whl", hash = "sha256:8f7c38605023688c89789f566959006af6d61c99eeeb9e58342eb780c5761e5e", size = 4557937, upload-time = "2026-06-13T20:36:42.967Z" }, +]