From b3fa1b061c9c74a63448cc08cc1047cb9f66467d Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 15:15:22 +0200 Subject: [PATCH 01/12] chore: add pre-commit hooks, GitHub Actions CI, dev deps, editorconfig --- .editorconfig | 19 +++++++++ .github/workflows/ci.yml | 86 ++++++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 55 +++++++++++++++++++++++++ .secrets.baseline | 2 + pyproject.toml | 42 ++++++++++++++++++++ 5 files changed, 204 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .secrets.baseline diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..338ea0b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{yaml,yml,toml,json}] +indent_size = 2 + +[*.rs] +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..41ace66 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +jobs: + # ── Job 1: lint (fast — blocks everything else if it fails) ────────────────── + lint: + name: Lint & type-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dev tools + run: pip install ruff mypy + + - run: ruff check . + - run: ruff format --check . + - run: mypy chem_engine/ --ignore-missing-imports + + # ── Job 2: build Rust extension + run tests ────────────────────────────────── + test: + name: Build & test + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install maturin and dev dependencies + run: pip install maturin pytest pytest-cov pytest-xdist + + - name: Build Rust extension (in-place) + run: maturin develop --release + + - name: Run tests in parallel with coverage + run: > + pytest tests/ + -n auto + --tb=short + -q + --cov=chem_engine + --cov-report=term-missing + --cov-fail-under=80 + --ignore=tests/test_correctness_vs_rdkit.py + + # ── Job 3: RDKit cross-validation (optional, runs on main only) ────────────── + rdkit-validation: + name: RDKit cross-validation + runs-on: ubuntu-latest + needs: test + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install maturin, RDKit and test deps + run: pip install maturin rdkit pytest pytest-xdist + + - name: Build Rust extension + run: maturin develop --release + + - name: Run RDKit cross-validation tests + run: pytest tests/test_correctness_vs_rdkit.py --tb=short -q + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..094d891 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,55 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-merge-conflict + - id: check-added-large-files + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.0 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ["--baseline", ".secrets.baseline"] + + # ── Affected-test selection (pre-commit) + full suite (pre-push) ───────────── + # + # NOTE: chem-engine is a Rust-Python extension (maturin). + # The extension must be built before running tests: + # maturin develop --release + # (or: pip install -e . --no-build-isolation) + # + # pytest-testmon (pre-commit): + # Skips tests whose covered lines haven't changed since last run. + # First run builds the .testmondata DB (full suite). + # + # pytest-xdist (pre-push): + # Full suite in parallel — authoritative green-light before push. + # + - repo: local + hooks: + - id: pytest-testmon + name: "pytest-testmon: run only affected tests" + language: system + entry: .venv/bin/pytest tests/ --testmon --ignore=tests/test_correctness_vs_rdkit.py + pass_filenames: false + types: [python] + stages: [pre-commit] + + - id: pytest-xdist-full + name: "pytest-xdist: full test suite in parallel (pre-push)" + language: system + entry: .venv/bin/pytest tests/ -n auto --ignore=tests/test_correctness_vs_rdkit.py + pass_filenames: false + types: [python] + stages: [pre-push] + diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..81eca94 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,2 @@ +{"version": "1.5.0", "plugins_used": [], "filters_used": [], "results": {}, "generated_at": ""} + diff --git a/pyproject.toml b/pyproject.toml index 6cfdb71..7a6b372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,3 +15,45 @@ dynamic = ["version"] [tool.maturin] features = ["pyo3/extension-module"] module-name = "chem_engine._rust" + +# ── dev dependencies ────────────────────────────────────────────────────────── +[dependency-groups] +dev = [ + "pytest>=8.0", + "pytest-cov>=5.0", + "ruff>=0.11", + "mypy>=1.0", + "pytest-testmon>=2.2.0", + "pytest-xdist>=3.8.0", +] + +# ── pytest ──────────────────────────────────────────────────────────────────── +[tool.pytest.ini_options] +testpaths = ["tests"] + +# ── coverage ────────────────────────────────────────────────────────────────── +[tool.coverage.run] +source = ["chem_engine"] +omit = ["tests/*"] + +[tool.coverage.report] +fail_under = 80 +show_missing = true + +# ── ruff ────────────────────────────────────────────────────────────────────── +[tool.ruff] +line-length = 100 +target-version = "py38" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "UP"] +ignore = [ + "S101", # allow assert in tests +] + +# ── mypy ────────────────────────────────────────────────────────────────────── +[tool.mypy] +python_version = "3.11" +strict = false +disallow_untyped_defs = false +ignore_missing_imports = true From e4e81592cb6296f8395f313f258667567ee63b15 Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 16:59:07 +0200 Subject: [PATCH 02/12] fix: resolve all ruff lint errors; switch CI/pre-commit setup to uv --- .github/workflows/ci.yml | 37 +-- .pre-commit-config.yaml | 11 +- benchmarks/extreme_scale_benchmark.py | 330 +++++++++++++++++--------- benchmarks/large_scale_benchmark.py | 133 ++++++----- chem_engine/__init__.py | 4 +- chem_engine/_rust.pyi | 3 +- chem_engine/utils.py | 9 +- pyproject.toml | 7 + tests/test_correctness_vs_rdkit.py | 180 +++++++------- tests/test_edge_cases.py | 63 ++--- tests/test_engine.py | 19 +- tests/test_invariants.py | 84 ++++--- tests/test_known_molecules.py | 141 +++++------ tests/test_substructure_extended.py | 83 ++++--- tests/test_tautomers_extended.py | 33 ++- 15 files changed, 652 insertions(+), 485 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41ace66..f55c849 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,17 +13,17 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + - uses: astral-sh/setup-uv@v6 with: + version: "latest" python-version: "3.11" - name: Install dev tools - run: pip install ruff mypy + run: uv sync - - run: ruff check . - - run: ruff format --check . - - run: mypy chem_engine/ --ignore-missing-imports + - run: uv run ruff check . + - run: uv run ruff format --check . + - run: uv run mypy chem_engine/ --ignore-missing-imports # ── Job 2: build Rust extension + run tests ────────────────────────────────── test: @@ -36,20 +36,20 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - - name: Set up Python - uses: actions/setup-python@v5 + - uses: astral-sh/setup-uv@v6 with: + version: "latest" python-version: "3.11" - - name: Install maturin and dev dependencies - run: pip install maturin pytest pytest-cov pytest-xdist + - name: Install dev dependencies + run: uv sync - name: Build Rust extension (in-place) - run: maturin develop --release + run: uv run maturin develop --release - name: Run tests in parallel with coverage run: > - pytest tests/ + uv run pytest tests/ -n auto --tb=short -q @@ -70,17 +70,18 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - - name: Set up Python - uses: actions/setup-python@v5 + - uses: astral-sh/setup-uv@v6 with: + version: "latest" python-version: "3.11" - - name: Install maturin, RDKit and test deps - run: pip install maturin rdkit pytest pytest-xdist + - name: Install dev dependencies + RDKit + run: uv sync && uv pip install rdkit - name: Build Rust extension - run: maturin develop --release + run: uv run maturin develop --release - name: Run RDKit cross-validation tests - run: pytest tests/test_correctness_vs_rdkit.py --tb=short -q + run: uv run pytest tests/test_correctness_vs_rdkit.py --tb=short -q + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 094d891..d6b4225 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,9 +24,13 @@ repos: # ── Affected-test selection (pre-commit) + full suite (pre-push) ───────────── # # NOTE: chem-engine is a Rust-Python extension (maturin). - # The extension must be built before running tests: - # maturin develop --release - # (or: pip install -e . --no-build-isolation) + # Build the extension and set up the dev environment with uv first: + # + # uv sync # install dev deps into .venv + # maturin develop --release # compile Rust extension in-place + # uv run detect-secrets scan > .secrets.baseline + # uv run pre-commit install --install-hooks + # uv run pre-commit install --hook-type pre-push # # pytest-testmon (pre-commit): # Skips tests whose covered lines haven't changed since last run. @@ -53,3 +57,4 @@ repos: types: [python] stages: [pre-push] + diff --git a/benchmarks/extreme_scale_benchmark.py b/benchmarks/extreme_scale_benchmark.py index 6ad48e7..c5d99af 100644 --- a/benchmarks/extreme_scale_benchmark.py +++ b/benchmarks/extreme_scale_benchmark.py @@ -73,14 +73,13 @@ import itertools import json import os -import statistics import sys -import time import threading +import time import warnings -from concurrent.futures import ProcessPoolExecutor, as_completed, Future +from concurrent.futures import Future, ProcessPoolExecutor, as_completed from pathlib import Path -from typing import Generator, Iterator +from typing import Iterator # ── process priority ───────────────────────────────────────────────────────── try: @@ -90,30 +89,34 @@ # ── RDKit / chem-engine imports ─────────────────────────────────────────────── from rdkit import RDLogger + RDLogger.DisableLog("rdApp.*") warnings.filterwarnings("ignore") import psutil -import chem_engine as ro from rdkit import Chem -from rdkit.Chem import AllChem, Descriptors, rdMolDescriptors +from rdkit.Chem import AllChem, rdMolDescriptors from rdkit.Chem.MolStandardize import rdMolStandardize +import chem_engine as ro + # ═══════════════════════════════════════════════════════════════════════════════ # Memory watchdog # ═══════════════════════════════════════════════════════════════════════════════ + class MemoryWatchdog: """ Background thread that monitors process RSS. When memory exceeds `limit_pct`% of total RAM, sets `self.paused = True`. Callers should check `watchdog.wait_if_needed()` before submitting new work. """ + def __init__(self, limit_pct: float = 70.0, poll_interval: float = 2.0): self._limit = limit_pct / 100.0 - self._poll = poll_interval + self._poll = poll_interval self._total = psutil.virtual_memory().total - self._stop = threading.Event() + self._stop = threading.Event() self.paused = False self._thread = threading.Thread(target=self._run, daemon=True) self._thread.start() @@ -146,11 +149,14 @@ def stop(self): # Module-level worker functions (must be picklable) # ═══════════════════════════════════════════════════════════════════════════════ + def _worker_validate(chunk): - import chem_engine as ro from rdkit import Chem - return [s for s in chunk - if _try_ce(ro, s) and Chem.MolFromSmiles(s) is not None] + + import chem_engine as ro + + return [s for s in chunk if _try_ce(ro, s) and Chem.MolFromSmiles(s) is not None] + def _try_ce(ro, s): try: @@ -159,77 +165,105 @@ def _try_ce(ro, s): except Exception: return False + def _worker_parse_ce(chunk): import chem_engine as ro + return [ro.parse_smiles(s) for s in chunk] + def _worker_parse_rk(chunk): from rdkit import Chem + return [Chem.MolFromSmiles(s) for s in chunk] + def _worker_canonical_ce(chunk): import chem_engine as ro + return [ro.canonicalize(ro.parse_smiles(s)) for s in chunk] + def _worker_canonical_rk(chunk): from rdkit import Chem + return [Chem.MolToSmiles(Chem.MolFromSmiles(s)) for s in chunk] + def _worker_amw_ce(chunk): import chem_engine as ro + return [ro.parse_smiles(s).amw for s in chunk] + def _worker_amw_rk(chunk): from rdkit import Chem, Descriptors + return [Descriptors.MolWt(Chem.MolFromSmiles(s)) for s in chunk] + def _worker_rotbonds_ce(chunk): import chem_engine as ro + return [ro.parse_smiles(s).num_rotatable_bonds for s in chunk] + def _worker_rotbonds_rk(chunk): from rdkit import Chem - from rdkit.Chem import rdMolDescriptors + return [rdMolDescriptors.CalcNumRotatableBonds(Chem.MolFromSmiles(s)) for s in chunk] + def _worker_substruct_ce(args): chunk, q_smi = args import chem_engine as ro + q = ro.parse_smiles(q_smi) return [ro.parse_smiles(s).has_substruct_match(q) for s in chunk] + def _worker_substruct_rk(args): chunk, q_smi = args from rdkit import Chem + q = Chem.MolFromSmarts(q_smi) return [Chem.MolFromSmiles(s).HasSubstructMatch(q) for s in chunk] + def _worker_tautomers_ce(chunk): import chem_engine as ro + return [ro.parse_smiles(s).enumerate_tautomers() for s in chunk] + def _worker_tautomers_rk(chunk): from rdkit import Chem - from rdkit.Chem.MolStandardize import rdMolStandardize + e = rdMolStandardize.TautomerEnumerator() return [e.Enumerate(Chem.MolFromSmiles(s)) for s in chunk] + def _worker_layout2d_ce(chunk): import chem_engine as ro + return [ro.generate_2d_coords(ro.parse_smiles(s)) for s in chunk] + def _worker_layout2d_rk(chunk): from rdkit import Chem - from rdkit.Chem import AllChem + return [AllChem.Compute2DCoords(Chem.MolFromSmiles(s)) for s in chunk] + def _worker_embed3d_ce(chunk): import chem_engine as ro + return [ro.generate_3d_coords(ro.parse_smiles(s)) for s in chunk] + def _worker_embed3d_rk(chunk): from rdkit import Chem - from rdkit.Chem import AllChem + out = [] for s in chunk: m = Chem.AddHs(Chem.MolFromSmiles(s)) @@ -241,10 +275,11 @@ def _worker_embed3d_rk(chunk): # SMILES streaming source # ═══════════════════════════════════════════════════════════════════════════════ + def _raw_smiles_stream(path: str, max_len: int) -> Iterator[str]: """Yield raw (unvalidated) SMILES strings from ChEMBL file, cycling infinitely.""" opener = gzip.open if str(path).endswith(".gz") else open - while True: # outer loop cycles the file for > 2.4 M requests + while True: # outer loop cycles the file for > 2.4 M requests with opener(path, "rt") as fh: for i, line in enumerate(fh): if i == 0: @@ -271,11 +306,12 @@ def _detect_smiles_column(line: str) -> str | None: parts = line.split("\t") if "\t" in line else line.split() if not parts: return None + # heuristic: SMILES columns contain C, c, N, O, (, [, = chars def looks_like_smiles(s: str) -> bool: return len(s) >= 2 and any(ch in s for ch in "CcNnOoSsPpFfBbIi([=#") - for col in [1, 0, 2]: # ChEMBL/PubChem col1, then ZINC/plain col0 + for col in [1, 0, 2]: # ChEMBL/PubChem col1, then ZINC/plain col0 if col < len(parts) and looks_like_smiles(parts[col]): return parts[col] return None @@ -353,8 +389,11 @@ def _drain(): break if read_total % 50_000 == 0: pct = min(100, len(valid) / pool_size * 100) - print(f"[pool] {pct:.0f}% ({len(valid):,}/{pool_size:,}," - f" read {read_total:,}) …", end="\r", flush=True) + print( + f"[pool] {pct:.0f}% ({len(valid):,}/{pool_size:,}, read {read_total:,}) …", + end="\r", + flush=True, + ) for f in as_completed(pending): for s in f.result(): @@ -366,13 +405,19 @@ def _drain(): valid = valid[:pool_size] unique_sources = len(paths) - print(f"\n[pool] Done - {len(valid):,} unique validated molecules " - f"from {unique_sources} source(s).", flush=True) + print( + f"\n[pool] Done - {len(valid):,} unique validated molecules " + f"from {unique_sources} source(s).", + flush=True, + ) if len(valid) < pool_size: shortfall = pool_size - len(valid) - print(f"[pool] WARNING: pool is {shortfall:,} short of target {pool_size:,}. " - f"Cycling pool to compensate.", flush=True) + print( + f"[pool] WARNING: pool is {shortfall:,} short of target {pool_size:,}. " + f"Cycling pool to compensate.", + flush=True, + ) if valid: extended = list(itertools.islice(itertools.cycle(valid), pool_size)) valid = extended @@ -381,17 +426,17 @@ def _drain(): # ── keep old single-source function as a thin wrapper ────────────────────── -def build_validated_pool(path: str, max_len: int, pool_size: int, - workers: int, chunk_size: int) -> list[str]: - return build_validated_pool_multi( - [path], max_len, pool_size, workers, chunk_size) - +def build_validated_pool( + path: str, max_len: int, pool_size: int, workers: int, chunk_size: int +) -> list[str]: + return build_validated_pool_multi([path], max_len, pool_size, workers, chunk_size) # ═══════════════════════════════════════════════════════════════════════════════ # Streaming throughput measurement # ═══════════════════════════════════════════════════════════════════════════════ + def streaming_throughput( ce_worker, rk_worker, @@ -431,7 +476,7 @@ def _flush_done(): done_futs, remaining = [], [] for f in pending: if f.done(): - f.result() # propagate exceptions + f.result() # propagate exceptions done_futs.append(f) else: remaining.append(f) @@ -469,7 +514,7 @@ def _flush_done(): # ── run chem-engine ────────────────────────────────────────────────────── ce_wall, ce_n = run_engine(ce_worker) - time.sleep(1.0) # brief pause between engines + time.sleep(1.0) # brief pause between engines # ── run RDKit ──────────────────────────────────────────────────────────── rk_wall, rk_n = run_engine(rk_worker) @@ -486,23 +531,26 @@ def _flush_done(): # Formatting helpers # ═══════════════════════════════════════════════════════════════════════════════ + def _fmt(mps: float) -> str: if mps >= 1_000_000: - return f"{mps/1e6:.2f} M mol/s" + return f"{mps / 1e6:.2f} M mol/s" if mps >= 1_000: - return f"{mps/1e3:.1f} K mol/s" + return f"{mps / 1e3:.1f} K mol/s" return f"{mps:.0f} mol/s" + def _ratio(ce: float, rk: float) -> str: r = rk / ce if ce > 0 else float("inf") sym = "✅" if r >= 1.0 else "❌" return f"{r:.1f}× {sym}" + def _scale_tag(n: int) -> str: if n >= 1_000_000: - return f"{n//1_000_000}M" + return f"{n // 1_000_000}M" if n >= 1_000: - return f"{n//1_000}K" + return f"{n // 1_000}K" return str(n) @@ -510,6 +558,7 @@ def _scale_tag(n: int) -> str: # Checkpoint helpers # ═══════════════════════════════════════════════════════════════════════════════ + def load_checkpoint(path: str) -> dict: if path and Path(path).exists(): try: @@ -521,6 +570,7 @@ def load_checkpoint(path: str) -> dict: print(f"[checkpoint] Could not load {path}: {e}") return {} + def save_checkpoint(path: str, results: dict): if not path: return @@ -534,6 +584,7 @@ def save_checkpoint(path: str, results: dict): # Main benchmark # ═══════════════════════════════════════════════════════════════════════════════ + def run_extreme_benchmarks( smiles_pool: list[str], scales: list[int], @@ -550,12 +601,13 @@ def run_extreme_benchmarks( print(" EXTREME-SCALE BENCHMARK: chem-engine vs RDKit") print(f" Pool size : {len(smiles_pool):,} validated molecules (cycled for larger N)") print(f" Scales : {[_scale_tag(s) for s in scales]}") - print(f" Workers : {workers}/{cpu_n} | chunk={chunk_size} | budget={time_budget}s | nice=+10") + print( + f" Workers : {workers}/{cpu_n} | chunk={chunk_size} | budget={time_budget}s | nice=+10" + ) print("═" * 78) # ── helper: run one operation at all scales ──────────────────────────── - def bench(label: str, ce_w, rk_w, - allowed_scales=None, extra_arg=None, note: str = ""): + def bench(label: str, ce_w, rk_w, allowed_scales=None, extra_arg=None, note: str = ""): scl = allowed_scales if allowed_scales is not None else scales print(f"\n### {label}", flush=True) if note: @@ -565,7 +617,9 @@ def bench(label: str, ce_w, rk_w, key = f"{label}@{_scale_tag(N)}" if key in results: ce_r, rk_r = results[key]["ce_mps"], results[key]["rk_mps"] - print(f" {_scale_tag(N):>6} [cached] CE: {_fmt(ce_r):>20} | RK: {_fmt(rk_r):>20} | {_ratio(ce_r, rk_r)}") + print( + f" {_scale_tag(N):>6} [cached] CE: {_fmt(ce_r):>20} | RK: {_fmt(rk_r):>20} | {_ratio(ce_r, rk_r)}" + ) continue if N > len(smiles_pool) and N > 10_000_000: @@ -574,19 +628,26 @@ def bench(label: str, ce_w, rk_w, print(f" {_scale_tag(N):>6} running …", end="\r", flush=True) (ce_mps, _), (rk_mps, _), n_done = streaming_throughput( - ce_w, rk_w, smiles_pool, N, - workers=workers, chunk_size=chunk_size, - watchdog=watchdog, time_budget=time_budget, + ce_w, + rk_w, + smiles_pool, + N, + workers=workers, + chunk_size=chunk_size, + watchdog=watchdog, + time_budget=time_budget, extra_arg=extra_arg, ) tag = "" if n_done >= N else f" [budget hit @ {_scale_tag(n_done)}]" - print(f" {_scale_tag(N):>6} CE: {_fmt(ce_mps):>20} | RK: {_fmt(rk_mps):>20} | {_ratio(ce_mps, rk_mps)}{tag}") + print( + f" {_scale_tag(N):>6} CE: {_fmt(ce_mps):>20} | RK: {_fmt(rk_mps):>20} | {_ratio(ce_mps, rk_mps)}{tag}" + ) results[key] = {"ce_mps": ce_mps, "rk_mps": rk_mps, "n_done": n_done} save_checkpoint(checkpoint_path, results) - time.sleep(2.0) # inter-operation cooldown + time.sleep(2.0) # inter-operation cooldown # ── 1. SMILES Parsing ───────────────────────────────────────────────── bench("1. SMILES Parsing", _worker_parse_ce, _worker_parse_rk) @@ -601,8 +662,9 @@ def bench(label: str, ce_w, rk_w, bench("4. Rotatable Bonds", _worker_rotbonds_ce, _worker_rotbonds_rk) # ── 5. Substructure Search ──────────────────────────────────────────── - bench("5. Substructure Search", _worker_substruct_ce, _worker_substruct_rk, - extra_arg="c1ccccc1") + bench( + "5. Substructure Search", _worker_substruct_ce, _worker_substruct_rk, extra_arg="c1ccccc1" + ) # ── 6. Parallel Batch Parse (chem-engine Rayon - in-process, no IPC) ── print("\n### 6. Parallel Batch Parse (chem-engine Rayon vs RDKit loop)", flush=True) @@ -611,14 +673,15 @@ def bench(label: str, ce_w, rk_w, key = f"6. Batch Parse@{_scale_tag(N)}" if key in results: ce_r, rk_r = results[key]["ce_mps"], results[key]["rk_mps"] - print(f" {_scale_tag(N):>6} [cached] CE: {_fmt(ce_r):>20} | RK: {_fmt(rk_r):>20} | {_ratio(ce_r, rk_r)}") + print( + f" {_scale_tag(N):>6} [cached] CE: {_fmt(ce_r):>20} | RK: {_fmt(rk_r):>20} | {_ratio(ce_r, rk_r)}" + ) continue # Process in rolling windows of pool size to avoid RAM blow-up - window = smiles_pool # use full pool each pass; cycle passes + window = smiles_pool # use full pool each pass; cycle passes passes_needed = max(1, N // len(window)) remainder = N - passes_needed * len(window) - actual_n = passes_needed * len(window) + remainder ce_times, rk_times = [], [] t_budget_start = time.perf_counter() @@ -652,7 +715,9 @@ def bench(label: str, ce_w, rk_w, ce_mps = ce_total_n / ce_total_t rk_mps = rk_total_n / rk_total_t - print(f" {_scale_tag(N):>6} CE: {_fmt(ce_mps):>20} | RK: {_fmt(rk_mps):>20} | {_ratio(ce_mps, rk_mps)}") + print( + f" {_scale_tag(N):>6} CE: {_fmt(ce_mps):>20} | RK: {_fmt(rk_mps):>20} | {_ratio(ce_mps, rk_mps)}" + ) results[key] = {"ce_mps": ce_mps, "rk_mps": rk_mps, "n_done": ce_total_n} save_checkpoint(checkpoint_path, results) time.sleep(2.0) @@ -663,8 +728,9 @@ def bench(label: str, ce_w, rk_w, key_sim = f"7. Tanimoto@{_scale_tag(SIM_N)}" if key_sim not in results: sim_ce = [ro.parse_smiles(s) for s in smiles_pool[:SIM_N]] - from rdkit.Chem import MorganGenerator from rdkit import DataStructs + from rdkit.Chem import MorganGenerator + gen = MorganGenerator.GetMorganGenerator(radius=2, fpSize=2048) sim_rk = [gen.GetFingerprint(Chem.MolFromSmiles(s)) for s in smiles_pool[:SIM_N]] n_pairs = SIM_N * (SIM_N - 1) // 2 @@ -683,28 +749,44 @@ def bench(label: str, ce_w, rk_w, ce_mps = n_pairs / ce_t rk_mps = n_pairs / rk_t - print(f" {SIM_N:,}²/2 = {n_pairs:,} pairs | CE: {_fmt(ce_mps):>20} | RK: {_fmt(rk_mps):>20} | {_ratio(ce_mps, rk_mps)}") + print( + f" {SIM_N:,}²/2 = {n_pairs:,} pairs | CE: {_fmt(ce_mps):>20} | RK: {_fmt(rk_mps):>20} | {_ratio(ce_mps, rk_mps)}" + ) results[key_sim] = {"ce_mps": ce_mps, "rk_mps": rk_mps, "n_done": n_pairs} save_checkpoint(checkpoint_path, results) else: r = results[key_sim] - print(f" [cached] CE: {_fmt(r['ce_mps']):>20} | RK: {_fmt(r['rk_mps']):>20} | {_ratio(r['ce_mps'], r['rk_mps'])}") + print( + f" [cached] CE: {_fmt(r['ce_mps']):>20} | RK: {_fmt(r['rk_mps']):>20} | {_ratio(r['ce_mps'], r['rk_mps'])}" + ) time.sleep(2.0) # ── 8. Tautomers (capped at 10K regardless of scale) ────────────────── - bench("8. Tautomers (10K cap)", _worker_tautomers_ce, _worker_tautomers_rk, - allowed_scales=[min(10_000, s) for s in [10_000]], - note="capped at 10K per scale to prevent multi-hour runs") + bench( + "8. Tautomers (10K cap)", + _worker_tautomers_ce, + _worker_tautomers_rk, + allowed_scales=[min(10_000, s) for s in [10_000]], + note="capped at 10K per scale to prevent multi-hour runs", + ) # ── 9. 2D Layout (capped at 10K) ────────────────────────────────────── - bench("9. 2D Layout (10K cap)", _worker_layout2d_ce, _worker_layout2d_rk, - allowed_scales=[10_000], - note="capped at 10K") + bench( + "9. 2D Layout (10K cap)", + _worker_layout2d_ce, + _worker_layout2d_rk, + allowed_scales=[10_000], + note="capped at 10K", + ) # ── 10. 3D Embedding (capped at 1K) ─────────────────────────────────── - bench("10. 3D Embedding (1K cap)", _worker_embed3d_ce, _worker_embed3d_rk, - allowed_scales=[1_000], - note="capped at 1K - ETKDG is O(N³)") + bench( + "10. 3D Embedding (1K cap)", + _worker_embed3d_ce, + _worker_embed3d_rk, + allowed_scales=[1_000], + note="capped at 1K - ETKDG is O(N³)", + ) # ── Summary ─────────────────────────────────────────────────────────── print("\n" + "═" * 78) @@ -723,9 +805,9 @@ def bench(label: str, ce_w, rk_w, print("═" * 78) wins = sum( - 1 for v in results.values() - if "ce_mps" in v and "rk_mps" in v - and v["ce_mps"] > 0 and v["rk_mps"] / v["ce_mps"] >= 1.0 + 1 + for v in results.values() + if "ce_mps" in v and "rk_mps" in v and v["ce_mps"] > 0 and v["rk_mps"] / v["ce_mps"] >= 1.0 ) total = sum(1 for v in results.values() if "ce_mps" in v) print(f"\n chem-engine faster in {wins}/{total} measured operations.\n") @@ -739,45 +821,74 @@ def bench(label: str, ce_w, rk_w, if __name__ == "__main__": cpu_n = os.cpu_count() or 2 - default_workers = max(1, cpu_n // 3) # conservative for extreme scale + default_workers = max(1, cpu_n // 3) # conservative for extreme scale parser = argparse.ArgumentParser( description="Extreme-scale (100K-10M) chem-engine vs RDKit benchmark", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - parser.add_argument("--sources", nargs="+", default=None, - metavar="FILE", - help=( - "One or more SMILES source files (gzip or plain). " - "Supports ChEMBL (TSV col 1), ZINC22 (space col 0), " - "PubChem (TSV col 1), and plain one-per-line. " - "Files are combined and deduplicated. " - "Example: --sources chembl.txt.gz zinc_lead.smi.gz pubchem.tsv.gz" - )) - parser.add_argument("--chembl", default=None, - help="Shorthand for --sources with a single ChEMBL file " - "(kept for backward compatibility).") + parser.add_argument( + "--sources", + nargs="+", + default=None, + metavar="FILE", + help=( + "One or more SMILES source files (gzip or plain). " + "Supports ChEMBL (TSV col 1), ZINC22 (space col 0), " + "PubChem (TSV col 1), and plain one-per-line. " + "Files are combined and deduplicated. " + "Example: --sources chembl.txt.gz zinc_lead.smi.gz pubchem.tsv.gz" + ), + ) + parser.add_argument( + "--chembl", + default=None, + help="Shorthand for --sources with a single ChEMBL file (kept for backward compatibility).", + ) parser.add_argument("--max-smiles-len", type=int, default=150) - parser.add_argument("--pool-size", type=int, default=200_000, - help="Validated molecule pool to build; cycled for larger N") - parser.add_argument("--workers", type=int, default=default_workers, - help=f"ProcessPoolExecutor workers (default: {default_workers} = cpu//3)") - parser.add_argument("--chunk-size", type=int, default=1_000, - help="Molecules per worker chunk (larger = less IPC overhead)") - parser.add_argument("--time-budget", type=float, default=600.0, - help="Max seconds per (operation × scale) pair") - parser.add_argument("--mem-limit-pct", type=float, default=70.0, - help="Pause new work when process RAM exceeds this %% of total") - parser.add_argument("--scales", default="100000,1000000,10000000", - help="Comma-separated molecule counts to benchmark") - parser.add_argument("--checkpoint", default="/tmp/chem_extreme_checkpoint.json", - help="JSON file for checkpoint/resume") + parser.add_argument( + "--pool-size", + type=int, + default=200_000, + help="Validated molecule pool to build; cycled for larger N", + ) + parser.add_argument( + "--workers", + type=int, + default=default_workers, + help=f"ProcessPoolExecutor workers (default: {default_workers} = cpu//3)", + ) + parser.add_argument( + "--chunk-size", + type=int, + default=1_000, + help="Molecules per worker chunk (larger = less IPC overhead)", + ) + parser.add_argument( + "--time-budget", type=float, default=600.0, help="Max seconds per (operation × scale) pair" + ) + parser.add_argument( + "--mem-limit-pct", + type=float, + default=70.0, + help="Pause new work when process RAM exceeds this %% of total", + ) + parser.add_argument( + "--scales", + default="100000,1000000,10000000", + help="Comma-separated molecule counts to benchmark", + ) + parser.add_argument( + "--checkpoint", + default="/tmp/chem_extreme_checkpoint.json", + help="JSON file for checkpoint/resume", + ) args = parser.parse_args() scales = sorted(set(int(x) for x in args.scales.split(","))) - args.workers = max(1, min(args.workers, cpu_n - 2)) # always leave ≥ 2 cores free + args.workers = max(1, min(args.workers, cpu_n - 2)) # always leave ≥ 2 cores free - print(f"\n[config]") + print("\n[config]") print(f" workers = {args.workers}/{cpu_n}") print(f" pool_size = {args.pool_size:,}") print(f" chunk_size = {args.chunk_size:,}") @@ -785,14 +896,16 @@ def bench(label: str, ce_w, rk_w, print(f" mem_limit = {args.mem_limit_pct}% of RAM") print(f" scales = {[_scale_tag(s) for s in scales]}") print(f" checkpoint = {args.checkpoint}") - print(f" nice = +10") + print(" nice = +10") # ── start memory watchdog ───────────────────────────────────────────── watchdog = MemoryWatchdog(limit_pct=args.mem_limit_pct) vm = psutil.virtual_memory() - print(f"\n[memory] Total: {vm.total/1e9:.1f} GB " - f"Available: {vm.available/1e9:.1f} GB " - f"Limit: {args.mem_limit_pct}% = {vm.total * args.mem_limit_pct/100/1e9:.1f} GB") + print( + f"\n[memory] Total: {vm.total / 1e9:.1f} GB " + f"Available: {vm.available / 1e9:.1f} GB " + f"Limit: {args.mem_limit_pct}% = {vm.total * args.mem_limit_pct / 100 / 1e9:.1f} GB" + ) # ── build validated SMILES pool ─────────────────────────────────────── sources = args.sources if args.sources else ([args.chembl] if args.chembl else []) @@ -802,8 +915,11 @@ def bench(label: str, ce_w, rk_w, sys.exit(1) pool = build_validated_pool_multi( - sources, args.max_smiles_len, - args.pool_size, args.workers, args.chunk_size, + sources, + args.max_smiles_len, + args.pool_size, + args.workers, + args.chunk_size, ) if len(pool) < 10_000: @@ -811,13 +927,16 @@ def bench(label: str, ce_w, rk_w, watchdog.stop() sys.exit(1) - print(f"[info] Pool of {len(pool):,} molecules will be cycled " - f"{max(scales) // len(pool) + 1}× to reach " - f"{_scale_tag(max(scales))} scale.\n") + print( + f"[info] Pool of {len(pool):,} molecules will be cycled " + f"{max(scales) // len(pool) + 1}× to reach " + f"{_scale_tag(max(scales))} scale.\n" + ) try: run_extreme_benchmarks( - pool, scales, + pool, + scales, workers=args.workers, chunk_size=args.chunk_size, time_budget=args.time_budget, @@ -826,8 +945,3 @@ def bench(label: str, ce_w, rk_w, ) finally: watchdog.stop() - - - - - diff --git a/benchmarks/large_scale_benchmark.py b/benchmarks/large_scale_benchmark.py index 0addc99..4a05107 100644 --- a/benchmarks/large_scale_benchmark.py +++ b/benchmarks/large_scale_benchmark.py @@ -20,29 +20,32 @@ import argparse import gzip -import time -import sys import statistics +import sys +import time import warnings -from pathlib import Path # Suppress RDKit kekulization warnings for large-scale runs from rdkit import RDLogger + RDLogger.DisableLog("rdApp.*") -import chem_engine as ro from rdkit import Chem from rdkit.Chem import AllChem, Descriptors, rdMolDescriptors from rdkit.Chem.MolStandardize import rdMolStandardize +import chem_engine as ro + warnings.filterwarnings("ignore") # --------------------------------------------------------------------------- # Dataset loading # --------------------------------------------------------------------------- -def load_chembl_smiles(path: str, max_mols: int = 2_000_000, - max_smiles_len: int = 150) -> list[str]: + +def load_chembl_smiles( + path: str, max_mols: int = 2_000_000, max_smiles_len: int = 150 +) -> list[str]: """ Load canonical SMILES from the ChEMBL chemreps TSV (gzip or plain). Filters out peptides / macromolecules (SMILES > max_smiles_len chars) @@ -91,6 +94,7 @@ def load_chembl_smiles(path: str, max_mols: int = 2_000_000, # Benchmark helpers # --------------------------------------------------------------------------- + def throughput(fn, n_mols: int, n_reps: int = 3) -> tuple[float, float]: """Run fn() n_reps times; return (mean_mols_per_sec, stdev).""" times = [] @@ -98,17 +102,15 @@ def throughput(fn, n_mols: int, n_reps: int = 3) -> tuple[float, float]: t0 = time.perf_counter() fn() times.append(time.perf_counter() - t0) - mean_t = statistics.mean(times) - stdev_t = statistics.stdev(times) if len(times) > 1 else 0.0 mps_vals = [n_mols / t for t in times] return statistics.mean(mps_vals), statistics.stdev(mps_vals) if len(mps_vals) > 1 else 0.0 def fmt(mps: float, sd: float) -> str: if mps >= 1_000_000: - return f"{mps/1e6:.2f} M mol/s (±{sd/1e6:.2f})" + return f"{mps / 1e6:.2f} M mol/s (±{sd / 1e6:.2f})" if mps >= 1_000: - return f"{mps/1e3:.1f} K mol/s (±{sd/1e3:.1f})" + return f"{mps / 1e3:.1f} K mol/s (±{sd / 1e3:.1f})" return f"{mps:.0f} mol/s (±{sd:.0f})" @@ -142,13 +144,13 @@ def run_benchmarks(smiles_all: list[str]): subset = smiles_all[:N] n_reps = max(1, min(5, 20_000 // N)) - ce_mps, ce_sd = throughput( - lambda s=subset: [ro.parse_smiles(x) for x in s], N, n_reps) - rk_mps, rk_sd = throughput( - lambda s=subset: [Chem.MolFromSmiles(x) for x in s], N, n_reps) + ce_mps, ce_sd = throughput(lambda s=subset: [ro.parse_smiles(x) for x in s], N, n_reps) + rk_mps, rk_sd = throughput(lambda s=subset: [Chem.MolFromSmiles(x) for x in s], N, n_reps) spd = ratio_str(ce_mps, rk_mps) - print(f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) parse_rows.append((N, ce_mps, rk_mps)) results[f"parse_{N}"] = (ce_mps, rk_mps) @@ -164,13 +166,13 @@ def run_benchmarks(smiles_all: list[str]): ce_mols = [ro.parse_smiles(s) for s in subset] rd_mols = [Chem.MolFromSmiles(s) for s in subset] - ce_mps, ce_sd = throughput( - lambda m=ce_mols: [ro.canonicalize(x) for x in m], N, n_reps) - rk_mps, rk_sd = throughput( - lambda m=rd_mols: [Chem.MolToSmiles(x) for x in m], N, n_reps) + ce_mps, ce_sd = throughput(lambda m=ce_mols: [ro.canonicalize(x) for x in m], N, n_reps) + rk_mps, rk_sd = throughput(lambda m=rd_mols: [Chem.MolToSmiles(x) for x in m], N, n_reps) spd = ratio_str(ce_mps, rk_mps) - print(f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) results[f"canonical_{N}"] = (ce_mps, rk_mps) # ----------------------------------------------------------------------- @@ -185,13 +187,13 @@ def run_benchmarks(smiles_all: list[str]): ce_mols = [ro.parse_smiles(s) for s in subset] rd_mols = [Chem.MolFromSmiles(s) for s in subset] - ce_mps, ce_sd = throughput( - lambda m=ce_mols: [x.amw for x in m], N, n_reps) - rk_mps, rk_sd = throughput( - lambda m=rd_mols: [Descriptors.MolWt(x) for x in m], N, n_reps) + ce_mps, ce_sd = throughput(lambda m=ce_mols: [x.amw for x in m], N, n_reps) + rk_mps, rk_sd = throughput(lambda m=rd_mols: [Descriptors.MolWt(x) for x in m], N, n_reps) spd = ratio_str(ce_mps, rk_mps) - print(f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) results[f"amw_{N}"] = (ce_mps, rk_mps) # ----------------------------------------------------------------------- @@ -206,13 +208,15 @@ def run_benchmarks(smiles_all: list[str]): ce_mols = [ro.parse_smiles(s) for s in subset] rd_mols = [Chem.MolFromSmiles(s) for s in subset] - ce_mps, ce_sd = throughput( - lambda m=ce_mols: [x.num_rotatable_bonds for x in m], N, n_reps) + ce_mps, ce_sd = throughput(lambda m=ce_mols: [x.num_rotatable_bonds for x in m], N, n_reps) rk_mps, rk_sd = throughput( - lambda m=rd_mols: [rdMolDescriptors.CalcNumRotatableBonds(x) for x in m], N, n_reps) + lambda m=rd_mols: [rdMolDescriptors.CalcNumRotatableBonds(x) for x in m], N, n_reps + ) spd = ratio_str(ce_mps, rk_mps) - print(f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) results[f"rotbonds_{N}"] = (ce_mps, rk_mps) # ----------------------------------------------------------------------- @@ -223,6 +227,7 @@ def run_benchmarks(smiles_all: list[str]): sim_subset = smiles_all[:SIM_N] ce_mols_sim = [ro.parse_smiles(s) for s in sim_subset] from rdkit.Chem import MorganGenerator + gen = MorganGenerator.GetMorganGenerator(radius=2, fpSize=2048) rd_fps = [gen.GetFingerprint(Chem.MolFromSmiles(s)) for s in sim_subset] from rdkit import DataStructs @@ -241,7 +246,9 @@ def rk_sim_fn(): ce_mps, ce_sd = throughput(ce_sim_fn, n_pairs, 3) rk_mps, rk_sd = throughput(rk_sim_fn, n_pairs, 3) spd = ratio_str(ce_mps, rk_mps) - print(f" N={SIM_N:>8,}²/2 = {n_pairs:,} pairs | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={SIM_N:>8,}²/2 = {n_pairs:,} pairs | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) results[f"tanimoto_{SIM_N}"] = (ce_mps, rk_mps) # ----------------------------------------------------------------------- @@ -259,12 +266,16 @@ def rk_sim_fn(): rd_mols_t = [Chem.MolFromSmiles(s) for s in subset] ce_mps, ce_sd = throughput( - lambda m=ce_mols_t: [x.enumerate_tautomers() for x in m], N, n_reps) + lambda m=ce_mols_t: [x.enumerate_tautomers() for x in m], N, n_reps + ) rk_mps, rk_sd = throughput( - lambda m=rd_mols_t, e=enumerator: [e.Enumerate(x) for x in m], N, n_reps) + lambda m=rd_mols_t, e=enumerator: [e.Enumerate(x) for x in m], N, n_reps + ) spd = ratio_str(ce_mps, rk_mps) - print(f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) results[f"tautomers_{N}"] = (ce_mps, rk_mps) # ----------------------------------------------------------------------- @@ -283,12 +294,16 @@ def rk_sim_fn(): rd_targets = [Chem.MolFromSmiles(s) for s in subset] ce_mps, ce_sd = throughput( - lambda t=ce_targets, q=query_ce: [x.has_substruct_match(q) for x in t], N, n_reps) + lambda t=ce_targets, q=query_ce: [x.has_substruct_match(q) for x in t], N, n_reps + ) rk_mps, rk_sd = throughput( - lambda t=rd_targets, q=query_rk: [x.HasSubstructMatch(q) for x in t], N, n_reps) + lambda t=rd_targets, q=query_rk: [x.HasSubstructMatch(q) for x in t], N, n_reps + ) spd = ratio_str(ce_mps, rk_mps) - print(f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) results[f"substruct_{N}"] = (ce_mps, rk_mps) # ----------------------------------------------------------------------- @@ -305,12 +320,16 @@ def rk_sim_fn(): rd_mols_2d = [Chem.MolFromSmiles(s) for s in subset] ce_mps, ce_sd = throughput( - lambda m=ce_mols_2d: [ro.generate_2d_coords(x) for x in m], N, n_reps) + lambda m=ce_mols_2d: [ro.generate_2d_coords(x) for x in m], N, n_reps + ) rk_mps, rk_sd = throughput( - lambda m=rd_mols_2d: [AllChem.Compute2DCoords(x) for x in m], N, n_reps) + lambda m=rd_mols_2d: [AllChem.Compute2DCoords(x) for x in m], N, n_reps + ) spd = ratio_str(ce_mps, rk_mps) - print(f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) results[f"layout2d_{N}"] = (ce_mps, rk_mps) # ----------------------------------------------------------------------- @@ -331,11 +350,14 @@ def rk_3d_fn(smis=subset): AllChem.EmbedMolecule(m, randomSeed=42) ce_mps, ce_sd = throughput( - lambda m=ce_mols_3d: [ro.generate_3d_coords(x) for x in m], N, n_reps) + lambda m=ce_mols_3d: [ro.generate_3d_coords(x) for x in m], N, n_reps + ) rk_mps, rk_sd = throughput(rk_3d_fn, N, n_reps) spd = ratio_str(ce_mps, rk_mps) - print(f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) results[f"embed3d_{N}"] = (ce_mps, rk_mps) # ----------------------------------------------------------------------- @@ -348,13 +370,13 @@ def rk_3d_fn(smis=subset): subset = smiles_all[:N] n_reps = max(1, min(5, 20_000 // N)) - ce_mps, ce_sd = throughput( - lambda s=subset: ro.batch_parse_smiles(s), N, n_reps) - rk_mps, rk_sd = throughput( - lambda s=subset: [Chem.MolFromSmiles(x) for x in s], N, n_reps) + ce_mps, ce_sd = throughput(lambda s=subset: ro.batch_parse_smiles(s), N, n_reps) + rk_mps, rk_sd = throughput(lambda s=subset: [Chem.MolFromSmiles(x) for x in s], N, n_reps) spd = ratio_str(ce_mps, rk_mps) - print(f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}") + print( + f" N={N:>8,} | CE: {fmt(ce_mps, ce_sd):>30} | RDKit: {fmt(rk_mps, rk_sd):>30} | Speedup: {spd}" + ) results[f"batch_{N}"] = (ce_mps, rk_mps) # ----------------------------------------------------------------------- @@ -381,12 +403,18 @@ def rk_3d_fn(smis=subset): if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--chembl", default="/tmp/chembl_37_chemreps.txt.gz", - help="Path to chembl_37_chemreps.txt[.gz]") - parser.add_argument("--max-mols", type=int, default=1_200_000, - help="Maximum molecules to load") - parser.add_argument("--max-smiles-len", type=int, default=150, - help="Filter out SMILES longer than this (removes macromolecules)") + parser.add_argument( + "--chembl", + default="/tmp/chembl_37_chemreps.txt.gz", + help="Path to chembl_37_chemreps.txt[.gz]", + ) + parser.add_argument("--max-mols", type=int, default=1_200_000, help="Maximum molecules to load") + parser.add_argument( + "--max-smiles-len", + type=int, + default=150, + help="Filter out SMILES longer than this (removes macromolecules)", + ) args = parser.parse_args() smiles = load_chembl_smiles(args.chembl, args.max_mols, args.max_smiles_len) @@ -395,4 +423,3 @@ def rk_3d_fn(smis=subset): sys.exit(1) run_benchmarks(smiles) - diff --git a/chem_engine/__init__.py b/chem_engine/__init__.py index 66951c4..ea85bb0 100644 --- a/chem_engine/__init__.py +++ b/chem_engine/__init__.py @@ -3,11 +3,11 @@ Bond, BondType, RustMolecule, - parse_smiles, + batch_parse_smiles, canonicalize, generate_2d_coords, generate_3d_coords, - batch_parse_smiles, + parse_smiles, ) __all__ = [ diff --git a/chem_engine/_rust.pyi b/chem_engine/_rust.pyi index eae92f3..f6ee96c 100644 --- a/chem_engine/_rust.pyi +++ b/chem_engine/_rust.pyi @@ -10,9 +10,8 @@ class BondType: class RustMolecule: pass - def parse_smiles(smiles: str) -> RustMolecule: ... def canonicalize(smiles: str) -> str: ... def generate_2d_coords(mol: RustMolecule): ... def generate_3d_coords(mol: RustMolecule): ... -def batch_parse_smiles(smiles: list[str]): ... \ No newline at end of file +def batch_parse_smiles(smiles: list[str]): ... diff --git a/chem_engine/utils.py b/chem_engine/utils.py index 1909334..47d18b6 100644 --- a/chem_engine/utils.py +++ b/chem_engine/utils.py @@ -6,7 +6,8 @@ e.g. get_num_atoms → .num_atoms get_coords_2d → .coords_2d """ -from ._rust import RustMolecule, Atom, Bond, BondType + +from ._rust import Atom, BondType, RustMolecule def to_rdkit(rust_mol: RustMolecule): @@ -45,14 +46,12 @@ def to_rdkit(rust_mol: RustMolecule): x, y, z = 0.0, 0.0, 0.0 symbol = atom.symbol - lines.append( - f"{x:10.4f}{y:10.4f}{z:10.4f} {symbol:<3s} 0 0 0 0 0 0 0 0 0 0 0 0" - ) + lines.append(f"{x:10.4f}{y:10.4f}{z:10.4f} {symbol:<3s} 0 0 0 0 0 0 0 0 0 0 0 0") # Bond block for j in range(num_bonds): bond = rust_mol.get_bond(j) - u = bond.source_idx + 1 # MolBlock uses 1-based indexing + u = bond.source_idx + 1 # MolBlock uses 1-based indexing v = bond.target_idx + 1 b_type = 1 # default: single diff --git a/pyproject.toml b/pyproject.toml index 7a6b372..dc7a406 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,13 @@ ignore = [ "S101", # allow assert in tests ] +[tool.ruff.lint.per-file-ignores] +# Benchmarks: intentional import ordering (RDKit warning suppression before +# remaining rdkit imports) + long formatted output lines are acceptable. +"benchmarks/**" = ["E402", "E501"] +# RDKit cross-validation: chem_engine must import after the try/except rdkit guard. +"tests/test_correctness_vs_rdkit.py" = ["E402"] + # ── mypy ────────────────────────────────────────────────────────────────────── [tool.mypy] python_version = "3.11" diff --git a/tests/test_correctness_vs_rdkit.py b/tests/test_correctness_vs_rdkit.py index 19c95b6..9ab0fa6 100644 --- a/tests/test_correctness_vs_rdkit.py +++ b/tests/test_correctness_vs_rdkit.py @@ -15,46 +15,46 @@ • AMW consistency (heavy-atom-only comparison) • Round-trip conversion (RustMol → rdkit.Mol → RustMol) """ + import pytest try: from rdkit import Chem - from rdkit.Chem import AllChem, Descriptors, rdMolDescriptors + from rdkit.Chem import AllChem, rdMolDescriptors + RDKIT_AVAILABLE = True except ImportError: RDKIT_AVAILABLE = False -pytestmark = pytest.mark.skipif( - not RDKIT_AVAILABLE, reason="RDKit not installed" -) +pytestmark = pytest.mark.skipif(not RDKIT_AVAILABLE, reason="RDKit not installed") import chem_engine as ro -from chem_engine.utils import to_rdkit, from_rdkit +from chem_engine.utils import from_rdkit, to_rdkit # ─── Reference molecules with known-correct values ────────────────────────── REFERENCE = [ # (name, smiles, n_heavy_atoms, n_bonds_heavy) - ("methane", "C", 1, 0), - ("ethanol", "CCO", 3, 2), - ("propane", "CCC", 3, 2), - ("butane", "CCCC", 4, 3), - ("benzene", "c1ccccc1", 6, 6), - ("naphthalene", "c1ccc2ccccc2c1", 10, 11), - ("pyridine", "c1ccncc1", 6, 6), - ("cyclohexane", "C1CCCCC1", 6, 6), - ("toluene", "Cc1ccccc1", 7, 7), - ("phenol", "Oc1ccccc1", 7, 7), - ("aniline", "Nc1ccccc1", 7, 7), - ("acetone", "CC(=O)C", 4, 3), - ("acetic_acid", "CC(=O)O", 4, 3), - ("formaldehyde", "C=O", 2, 1), - ("acetylene", "C#C", 2, 1), - ("aspirin", "CC(=O)Oc1ccccc1C(=O)O", 13, 13), - ("paracetamol", "CC(=O)Nc1ccc(O)cc1", 11, 11), - ("caffeine", "Cn1cnc2c1c(=O)n(C)c(=O)n2C", 14, 15), - ("ibuprofen", "CC(C)Cc1ccc(cc1)C(C)C(=O)O", 15, 15), - ("dopamine", "NCCc1ccc(O)c(O)c1", 11, 11), + ("methane", "C", 1, 0), + ("ethanol", "CCO", 3, 2), + ("propane", "CCC", 3, 2), + ("butane", "CCCC", 4, 3), + ("benzene", "c1ccccc1", 6, 6), + ("naphthalene", "c1ccc2ccccc2c1", 10, 11), + ("pyridine", "c1ccncc1", 6, 6), + ("cyclohexane", "C1CCCCC1", 6, 6), + ("toluene", "Cc1ccccc1", 7, 7), + ("phenol", "Oc1ccccc1", 7, 7), + ("aniline", "Nc1ccccc1", 7, 7), + ("acetone", "CC(=O)C", 4, 3), + ("acetic_acid", "CC(=O)O", 4, 3), + ("formaldehyde", "C=O", 2, 1), + ("acetylene", "C#C", 2, 1), + ("aspirin", "CC(=O)Oc1ccccc1C(=O)O", 13, 13), + ("paracetamol", "CC(=O)Nc1ccc(O)cc1", 11, 11), + ("caffeine", "Cn1cnc2c1c(=O)n(C)c(=O)n2C", 14, 15), + ("ibuprofen", "CC(C)Cc1ccc(cc1)C(C)C(=O)O", 15, 15), + ("dopamine", "NCCc1ccc(O)c(O)c1", 11, 11), ] @@ -66,15 +66,17 @@ def test_atom_count_matches_rdkit(self, name, smi, n_atoms, n_bonds): ce_mol = ro.parse_smiles(smi) rd_mol = Chem.MolFromSmiles(smi) assert rd_mol is not None, f"RDKit failed to parse {smi}" - assert ce_mol.num_atoms == rd_mol.GetNumAtoms(), \ + assert ce_mol.num_atoms == rd_mol.GetNumAtoms(), ( f"{name}: CE={ce_mol.num_atoms} vs RDKit={rd_mol.GetNumAtoms()}" + ) @pytest.mark.parametrize("name,smi,n_atoms,n_bonds", REFERENCE) def test_bond_count_matches_rdkit(self, name, smi, n_atoms, n_bonds): ce_mol = ro.parse_smiles(smi) rd_mol = Chem.MolFromSmiles(smi) - assert ce_mol.num_bonds == rd_mol.GetNumBonds(), \ + assert ce_mol.num_bonds == rd_mol.GetNumBonds(), ( f"{name}: CE={ce_mol.num_bonds} vs RDKit={rd_mol.GetNumBonds()}" + ) class TestElementSymbolsVsRDKit: @@ -87,40 +89,50 @@ def test_element_symbols(self, name, smi, _n, _b): for i in range(rd_mol.GetNumAtoms()): rd_sym = rd_mol.GetAtomWithIdx(i).GetSymbol() ce_sym = ce_mol.get_atom(i).symbol - assert ce_sym == rd_sym, \ - f"{name} atom {i}: CE={ce_sym!r} vs RDKit={rd_sym!r}" + assert ce_sym == rd_sym, f"{name} atom {i}: CE={ce_sym!r} vs RDKit={rd_sym!r}" class TestAromaticityVsRDKit: """Aromatic atom flags should agree with RDKit.""" - @pytest.mark.parametrize("smi", [ - "c1ccccc1", "c1ccncc1", "c1ccoc1", "c1ccsc1", "c1cc[nH]c1", - "c1ccc2ccccc2c1", # naphthalene - "Cc1ccccc1", # toluene - ]) + @pytest.mark.parametrize( + "smi", + [ + "c1ccccc1", + "c1ccncc1", + "c1ccoc1", + "c1ccsc1", + "c1cc[nH]c1", + "c1ccc2ccccc2c1", # naphthalene + "Cc1ccccc1", # toluene + ], + ) def test_aromatic_flags(self, smi): ce_mol = ro.parse_smiles(smi) rd_mol = Chem.MolFromSmiles(smi) for i in range(rd_mol.GetNumAtoms()): rd_arom = rd_mol.GetAtomWithIdx(i).GetIsAromatic() ce_arom = ce_mol.get_atom(i).is_aromatic - assert ce_arom == rd_arom, \ - f"{smi} atom {i}: CE={ce_arom} vs RDKit={rd_arom}" + assert ce_arom == rd_arom, f"{smi} atom {i}: CE={ce_arom} vs RDKit={rd_arom}" class TestBondTypesVsRDKit: """Bond type mapping vs RDKit - with known limitations documented.""" - BOND_TYPE_MAP = { - Chem.rdchem.BondType.SINGLE: ro.BondType.Single, - Chem.rdchem.BondType.DOUBLE: ro.BondType.Double, - Chem.rdchem.BondType.TRIPLE: ro.BondType.Triple, - Chem.rdchem.BondType.AROMATIC: ro.BondType.Aromatic, - } if RDKIT_AVAILABLE else {} - - @pytest.mark.parametrize("smi", ["CC", "C=C", "C#C", "C=O", "C#N", - "c1ccccc1", "CC(=O)O", "C1CCCCC1"]) + BOND_TYPE_MAP = ( + { + Chem.rdchem.BondType.SINGLE: ro.BondType.Single, + Chem.rdchem.BondType.DOUBLE: ro.BondType.Double, + Chem.rdchem.BondType.TRIPLE: ro.BondType.Triple, + Chem.rdchem.BondType.AROMATIC: ro.BondType.Aromatic, + } + if RDKIT_AVAILABLE + else {} + ) + + @pytest.mark.parametrize( + "smi", ["CC", "C=C", "C#C", "C=O", "C#N", "c1ccccc1", "CC(=O)O", "C1CCCCC1"] + ) def test_bond_types_non_aromatic(self, smi): ce_mol = ro.parse_smiles(smi) rd_mol = Chem.MolFromSmiles(smi) @@ -130,8 +142,9 @@ def test_bond_types_non_aromatic(self, smi): expected_ce_bt = self.BOND_TYPE_MAP.get(rd_bt) if expected_ce_bt is None: continue - assert ce_bt == expected_ce_bt, \ + assert ce_bt == expected_ce_bt, ( f"{smi} bond {i}: CE={ce_bt} vs expected={expected_ce_bt}" + ) @pytest.mark.parametrize("smi", ["c1ccccc1"]) def test_bond_types_aromatic(self, smi): @@ -147,29 +160,33 @@ def test_bond_types_aromatic(self, smi): class TestRotatableBondsVsRDKit: """Rotatable bond count must match RDKit (tolerance ±1 for edge cases).""" - @pytest.mark.parametrize("smi,expected_rk", [ - ("CC", 0), - ("CCC", 0), - ("CCCC", 1), - ("CCCCC", 2), - ("CCCCCC", 3), - ("c1ccccc1", 0), - ("C1CCCCC1", 0), - ("Cc1ccccc1", 0), - ("CCc1ccccc1", 1), - ("CCCc1ccccc1", 2), - ("CC(=O)O", 0), # terminal O on C=O - ("CCCO", 1), # C-C-C-O, middle C-C is rotatable - ("CCCCO", 2), - ]) + @pytest.mark.parametrize( + "smi,expected_rk", + [ + ("CC", 0), + ("CCC", 0), + ("CCCC", 1), + ("CCCCC", 2), + ("CCCCCC", 3), + ("c1ccccc1", 0), + ("C1CCCCC1", 0), + ("Cc1ccccc1", 0), + ("CCc1ccccc1", 1), + ("CCCc1ccccc1", 2), + ("CC(=O)O", 0), # terminal O on C=O + ("CCCO", 1), # C-C-C-O, middle C-C is rotatable + ("CCCCO", 2), + ], + ) def test_rotatable_bonds(self, smi, expected_rk): ce_mol = ro.parse_smiles(smi) rd_mol = Chem.MolFromSmiles(smi) rk_rot = rdMolDescriptors.CalcNumRotatableBonds(rd_mol) ce_rot = ce_mol.num_rotatable_bonds # Allow ±1 tolerance (definitions differ slightly for terminal groups) - assert abs(ce_rot - rk_rot) <= 1, \ + assert abs(ce_rot - rk_rot) <= 1, ( f"{smi}: CE={ce_rot}, RDKit={rk_rot}, expected~{expected_rk}" + ) class TestRdKitRoundTrip: @@ -198,29 +215,27 @@ def test_to_rdkit_returns_valid_mol(self, smi): def test_full_round_trip_atom_count(self, name, smi, _n, _b): """CE → RDKit → CE: atom count preserved.""" ce1 = ro.parse_smiles(smi) - rd = to_rdkit(ce1) + rd = to_rdkit(ce1) ce2 = from_rdkit(rd) - assert ce2.num_atoms == ce1.num_atoms, \ - f"{name}: {ce1.num_atoms} → {ce2.num_atoms}" + assert ce2.num_atoms == ce1.num_atoms, f"{name}: {ce1.num_atoms} → {ce2.num_atoms}" @pytest.mark.parametrize("name,smi,_n,_b", REFERENCE) def test_full_round_trip_bond_count(self, name, smi, _n, _b): ce1 = ro.parse_smiles(smi) - rd = to_rdkit(ce1) + rd = to_rdkit(ce1) ce2 = from_rdkit(rd) assert ce2.num_bonds == ce1.num_bonds def test_double_bond_preserved_round_trip(self): ce1 = ro.parse_smiles("C=O") - rd = to_rdkit(ce1) + rd = to_rdkit(ce1) bond = rd.GetBondWithIdx(0) assert bond.GetBondTypeAsDouble() == 2.0 def test_triple_bond_preserved_round_trip(self): ce1 = ro.parse_smiles("C#N") - rd = to_rdkit(ce1) - triple = [b for b in rd.GetBonds() - if b.GetBondTypeAsDouble() == 3.0] + rd = to_rdkit(ce1) + triple = [b for b in rd.GetBonds() if b.GetBondTypeAsDouble() == 3.0] assert len(triple) == 1 def test_3d_coords_preserved_round_trip(self): @@ -250,35 +265,28 @@ def test_cyclohexane_benzene_less_similar_than_cyclohexane_cyclohexane(self): def test_ethanol_ethylamine_more_similar_to_each_other_than_to_methane(self): """Ethanol and ethylamine share a 2-carbon chain; methane shares only C.""" - ethanol = ro.parse_smiles("CCO") + ethanol = ro.parse_smiles("CCO") ethylamine = ro.parse_smiles("CCN") - methane = ro.parse_smiles("C") + methane = ro.parse_smiles("C") assert ethanol.similarity(ethylamine) > ethanol.similarity(methane) def test_closely_related_more_similar_than_unrelated(self): """ Ethanol/ethylamine vs ethanol/naphthalene ordering must agree with RDKit. """ - from rdkit.Chem import AllChem from rdkit import DataStructs + from rdkit.Chem import AllChem - gen_fps = lambda m: AllChem.GetMorganFingerprintAsBitVect(m, 2, 2048) + def gen_fps(m): + return AllChem.GetMorganFingerprintAsBitVect(m, 2, 2048) smis = ["CCO", "CCN", "c1ccc2ccccc2c1"] rd_mols = [Chem.MolFromSmiles(s) for s in smis] ce_mols = [ro.parse_smiles(s) for s in smis] rd_fps = [gen_fps(m) for m in rd_mols] - rk_order = DataStructs.TanimotoSimilarity(rd_fps[0], rd_fps[1]) > \ - DataStructs.TanimotoSimilarity(rd_fps[0], rd_fps[2]) - ce_order = ce_mols[0].similarity(ce_mols[1]) > \ - ce_mols[0].similarity(ce_mols[2]) + rk_order = DataStructs.TanimotoSimilarity( + rd_fps[0], rd_fps[1] + ) > DataStructs.TanimotoSimilarity(rd_fps[0], rd_fps[2]) + ce_order = ce_mols[0].similarity(ce_mols[1]) > ce_mols[0].similarity(ce_mols[2]) assert rk_order == ce_order - - - - - - - - diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index aa92726..5b34c05 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -16,14 +16,15 @@ • Isotope labels • Molecules with all atom types """ + import pytest -import chem_engine as ro +import chem_engine as ro # ─── Empty and minimal molecules ──────────────────────────────────────────── -class TestEmptyAndMinimal: +class TestEmptyAndMinimal: def test_empty_molecule_num_atoms_zero(self): m = ro.RustMolecule() assert m.num_atoms == 0 @@ -58,7 +59,7 @@ def test_empty_molecule_no_coords(self): def test_empty_molecule_substruct_empty_query(self): """Empty molecule contains empty query.""" target = ro.RustMolecule() - query = ro.RustMolecule() + query = ro.RustMolecule() assert target.has_substruct_match(query) is True def test_single_atom_carbon(self): @@ -88,8 +89,8 @@ def test_single_bond_molecule(self): # ─── Charged atoms ────────────────────────────────────────────────────────── -class TestChargedAtoms: +class TestChargedAtoms: def test_ammonium_positive_charge(self): m = ro.parse_smiles("[NH4+]") a = m.get_atom(0) @@ -131,8 +132,8 @@ def test_zwitterion_both_charges(self): # ─── Explicit hydrogen bracket atoms ──────────────────────────────────────── -class TestExplicitHydrogens: +class TestExplicitHydrogens: def test_explicit_h_count_ammonium(self): m = ro.parse_smiles("[NH4+]") a = m.get_atom(0) @@ -146,8 +147,7 @@ def test_explicit_h_count_water(self): def test_bracket_nh2(self): m = ro.parse_smiles("[NH2]c1ccccc1") # aniline bracket form - n_atom = next(m.get_atom(i) for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 7) + n_atom = next(m.get_atom(i) for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 7) assert n_atom.num_explicit_hs == 2 def test_h_atom_atomic_number_1(self): @@ -158,8 +158,8 @@ def test_h_atom_atomic_number_1(self): # ─── Halogens ──────────────────────────────────────────────────────────────── -class TestHalogens: +class TestHalogens: def test_fluoromethane(self): m = ro.parse_smiles("CF") symbols = {m.get_atom(i).symbol for i in range(m.num_atoms)} @@ -168,33 +168,29 @@ def test_fluoromethane(self): def test_chloromethane_atomic_number(self): m = ro.parse_smiles("CCl") - cl_atoms = [m.get_atom(i) for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 17] + cl_atoms = [m.get_atom(i) for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 17] assert len(cl_atoms) == 1 def test_bromomethane_atomic_number(self): m = ro.parse_smiles("CBr") - br_atoms = [m.get_atom(i) for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 35] + br_atoms = [m.get_atom(i) for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 35] assert len(br_atoms) == 1 def test_iodomethane(self): m = ro.parse_smiles("CI") - i_atoms = [m.get_atom(i) for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 53] + i_atoms = [m.get_atom(i) for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 53] assert len(i_atoms) == 1 def test_perfluorobenzene_6_fluorines(self): m = ro.parse_smiles("Fc1c(F)c(F)c(F)c(F)c1F") - f_count = sum(1 for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 9) + f_count = sum(1 for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 9) assert f_count == 6 # ─── Ring closure edge cases ───────────────────────────────────────────────── -class TestRingClosures: +class TestRingClosures: def test_3_membered_ring(self): """Cyclopropane C1CC1 - 3 atoms, 3 bonds.""" m = ro.parse_smiles("C1CC1") @@ -221,7 +217,7 @@ def test_large_ring_closure_double_digit(self): def test_bicyclic_decalin(self): """Decalin C1CCCCC2CCCCC12 - 11 atoms, 12 bonds (chem-engine's ring-closure count).""" m = ro.parse_smiles("C1CCCCC2CCCCC12") - assert m.num_atoms == 11 # includes shared ring-junction atom + assert m.num_atoms == 11 # includes shared ring-junction atom assert m.num_bonds == 12 def test_spiro_compound(self): @@ -233,8 +229,8 @@ def test_spiro_compound(self): # ─── Long-chain stress ─────────────────────────────────────────────────────── -class TestLongChains: +class TestLongChains: def test_c20_chain_atom_count(self): smi = "C" * 20 m = ro.parse_smiles(smi) @@ -246,7 +242,10 @@ def test_c20_chain_bond_count(self): assert m.num_bonds == 19 def test_c20_rotatable_bonds_count(self): - """C20 linear chain: 17 rotatable bonds (C1-C2 through C17-C18; both terminal C-C are excluded).""" + """C20 linear chain: 17 rotatable bonds. + + C1-C2 through C17-C18; both terminal C-C bonds are excluded. + """ smi = "C" * 20 m = ro.parse_smiles(smi) assert m.num_rotatable_bonds == 17 @@ -260,8 +259,8 @@ def test_c50_chain(self): # ─── Out-of-bounds index access ────────────────────────────────────────────── -class TestOutOfBoundsAccess: +class TestOutOfBoundsAccess: def test_get_atom_out_of_range_returns_none(self): m = ro.parse_smiles("CCO") result = m.get_atom(100) @@ -289,8 +288,8 @@ def test_find_bond_invalid_indices(self): # ─── Invalid SMILES ────────────────────────────────────────────────────────── -class TestInvalidSmiles: +class TestInvalidSmiles: def test_completely_invalid(self): with pytest.raises(Exception): ro.parse_smiles("XYZ_INVALID_SMILES_999") @@ -322,8 +321,8 @@ def test_mismatched_brackets(self): # ─── Coordinate setter edge cases ──────────────────────────────────────────── -class TestCoordinateSetters: +class TestCoordinateSetters: def test_set_2d_coords_correct_count(self): m = ro.parse_smiles("CCO") m.coords_2d = [[0.0, 0.0], [1.5, 0.0], [3.0, 0.0]] @@ -371,8 +370,8 @@ def test_3d_embed_produces_coords(self): # ─── Batch parse edge cases ────────────────────────────────────────────────── -class TestBatchEdgeCases: +class TestBatchEdgeCases: def test_batch_empty_list(self): assert ro.batch_parse_smiles([]) == [] @@ -386,8 +385,7 @@ def test_batch_preserves_order(self): results = ro.batch_parse_smiles(smiles) assert len(results) == 5 for i, r in enumerate(results): - assert r.num_atoms == i + 1, \ - f"Position {i}: expected {i+1} atoms, got {r.num_atoms}" + assert r.num_atoms == i + 1, f"Position {i}: expected {i + 1} atoms, got {r.num_atoms}" def test_batch_diverse_elements(self): smiles = ["CCO", "CCN", "CCS", "CF", "CCl", "CBr", "CI"] @@ -395,8 +393,9 @@ def test_batch_diverse_elements(self): results = ro.batch_parse_smiles(smiles) assert len(results) == 7 for i, (r, exp) in enumerate(zip(results, expected_atoms)): - assert r.num_atoms == exp, \ + assert r.num_atoms == exp, ( f"Position {i} ({smiles[i]}): expected {exp} atoms, got {r.num_atoms}" + ) def test_batch_100_identical(self): results = ro.batch_parse_smiles(["c1ccccc1"] * 100) @@ -407,13 +406,3 @@ def test_batch_1000_mixed(self): smiles_pool = ["CCO", "CCCC", "c1ccccc1", "CN", "CC(=O)O"] * 200 results = ro.batch_parse_smiles(smiles_pool) assert len(results) == 1000 - - - - - - - - - - diff --git a/tests/test_engine.py b/tests/test_engine.py index a0c4de2..59462f6 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -10,15 +10,17 @@ get_coords_2d -> coords_2d get_coords_3d -> coords_3d """ + import pytest -import chem_engine as ro -from chem_engine.utils import to_rdkit, from_rdkit +import chem_engine as ro +from chem_engine.utils import from_rdkit, to_rdkit # --------------------------------------------------------------------------- # FR-1 & FR-2: Molecular graph + SMILES parsing # --------------------------------------------------------------------------- + class TestSmilesParsing: """FR-1 (Molecular Graph) + FR-2 (SMILES → 2D Pipeline)""" @@ -115,6 +117,7 @@ def test_invalid_smiles_raises(self): # FR-5: Canonical SMILES # --------------------------------------------------------------------------- + class TestCanonicalSmiles: """FR-5: 2D representation → canonical SMILES""" @@ -154,6 +157,7 @@ def test_canonical_nonzero_length(self): # FR-6: 2D and 3D coordinate generation # --------------------------------------------------------------------------- + class TestLayout: """FR-6: ETKDG-like 2D/3D coordinate generation""" @@ -198,6 +202,7 @@ def test_no_coords_initially(self): # FR-8: Descriptor calculations (AMW + RotBonds) # --------------------------------------------------------------------------- + class TestDescriptors: """FR-8: AMW and rotatable bonds""" @@ -236,6 +241,7 @@ def test_rotatable_bonds_ring(self): # FR-4: Parallel batch engine # --------------------------------------------------------------------------- + class TestBatchProcessing: """FR-4: Rayon-backed parallel SMILES parsing""" @@ -247,8 +253,8 @@ def test_batch_parse_returns_correct_count(self): def test_batch_parse_atom_counts(self): smiles_list = ["CCO", "CCCC"] results = ro.batch_parse_smiles(smiles_list) - assert results[0].num_atoms == 3 # CCO - assert results[1].num_atoms == 4 # CCCC + assert results[0].num_atoms == 3 # CCO + assert results[1].num_atoms == 4 # CCCC def test_batch_parse_empty_list(self): results = ro.batch_parse_smiles([]) @@ -266,6 +272,7 @@ def test_batch_parse_large(self): # FR-9: Substructure search # --------------------------------------------------------------------------- + class TestSubstructureSearch: """FR-9: Subgraph isomorphism""" @@ -309,6 +316,7 @@ def test_nitrogen_query(self): # FR-10: Chemical fingerprints + Tanimoto similarity # --------------------------------------------------------------------------- + class TestSimilaritySearch: """FR-10: Fingerprint generation and Tanimoto coefficient""" @@ -359,6 +367,7 @@ def test_empty_mol_similarity(self): # FR-7: Tautomer enumeration and standardization # --------------------------------------------------------------------------- + class TestTautomers: """FR-7: Tautomer enumeration and canonical tautomer selection""" @@ -407,6 +416,7 @@ def test_simple_mol_unchanged_tautomers(self): # FR-3 / FR-11: RDKit type interoperability # --------------------------------------------------------------------------- + class TestRDKitInterop: """FR-3 / FR-11: Two-way conversion with rdkit.Chem.Mol""" @@ -481,6 +491,7 @@ def test_to_rdkit_bond_types(self): # Manual atom/bond API (FR-1) # --------------------------------------------------------------------------- + class TestManualMoleculeConstruction: """FR-1: Direct Atom/Bond API""" diff --git a/tests/test_invariants.py b/tests/test_invariants.py index 71241bf..5cd3612 100644 --- a/tests/test_invariants.py +++ b/tests/test_invariants.py @@ -13,29 +13,37 @@ • Coordinate count equals atom count • AMW ordering invariants """ + import math + import pytest + import chem_engine as ro REFERENCE_SMILES = [ - "CCO", "CC(C)C", "C1CCCCC1", "c1ccccc1", - "CC(=O)O", "CN", "CCN", "CC(=O)N", - "CC(C)Cc1ccc(cc1)C(C)C(=O)O", # ibuprofen - "CC(=O)Oc1ccccc1C(=O)O", # aspirin - "Cn1cnc2c1c(=O)n(C)c(=O)n2C", # caffeine + "CCO", + "CC(C)C", + "C1CCCCC1", + "c1ccccc1", + "CC(=O)O", + "CN", + "CCN", + "CC(=O)N", + "CC(C)Cc1ccc(cc1)C(C)C(=O)O", # ibuprofen + "CC(=O)Oc1ccccc1C(=O)O", # aspirin + "Cn1cnc2c1c(=O)n(C)c(=O)n2C", # caffeine ] # ─── Tanimoto / Similarity invariants ──────────────────────────────────────── -class TestSimilarityInvariants: +class TestSimilarityInvariants: def test_self_similarity_always_one(self): """sim(A, A) == 1.0 for any non-empty molecule.""" for smi in REFERENCE_SMILES: m = ro.parse_smiles(smi) - assert abs(m.similarity(m) - 1.0) < 1e-9, \ - f"Self-similarity != 1 for {smi}" + assert abs(m.similarity(m) - 1.0) < 1e-9, f"Self-similarity != 1 for {smi}" def test_similarity_range_0_to_1(self): """0 ≤ sim(A, B) ≤ 1 for all molecule pairs.""" @@ -43,8 +51,7 @@ def test_similarity_range_0_to_1(self): for i, a in enumerate(mols): for j, b in enumerate(mols): s = a.similarity(b) - assert 0.0 <= s <= 1.0, \ - f"Similarity out of range [{i},{j}]: {s}" + assert 0.0 <= s <= 1.0, f"Similarity out of range [{i},{j}]: {s}" def test_similarity_symmetric(self): """sim(A, B) == sim(B, A).""" @@ -53,19 +60,20 @@ def test_similarity_symmetric(self): for j in range(i + 1, len(mols)): s_ij = mols[i].similarity(mols[j]) s_ji = mols[j].similarity(mols[i]) - assert abs(s_ij - s_ji) < 1e-9, \ + assert abs(s_ij - s_ji) < 1e-9, ( f"Similarity not symmetric at [{i},{j}]: {s_ij} vs {s_ji}" + ) def test_similar_more_than_dissimilar(self): """Ethanol/ethylamine should be more similar to each other than to naphthalene.""" - ethanol = ro.parse_smiles("CCO") + ethanol = ro.parse_smiles("CCO") ethylamine = ro.parse_smiles("CCN") naphthalene = ro.parse_smiles("c1ccc2ccccc2c1") assert ethanol.similarity(ethylamine) > ethanol.similarity(naphthalene) def test_empty_mol_similarity_zero(self): empty = ro.RustMolecule() - real = ro.parse_smiles("CCO") + real = ro.parse_smiles("CCO") assert empty.similarity(real) == 0.0 assert real.similarity(empty) == 0.0 @@ -77,7 +85,6 @@ def test_structurally_different_low_similarity(self): class TestFingerprintInvariants: - def test_fingerprint_length_always_2048(self): for smi in REFERENCE_SMILES + [""]: try: @@ -121,8 +128,8 @@ def test_same_molecule_same_fingerprint(self): # ─── Canonical SMILES invariants ───────────────────────────────────────────── -class TestCanonicalSmilesInvariants: +class TestCanonicalSmilesInvariants: def test_idempotent_on_all_reference_mols(self): for smi in REFERENCE_SMILES: m1 = ro.parse_smiles(smi) @@ -155,61 +162,57 @@ def test_canonical_different_for_different_structure(self): # ─── Substructure invariants ───────────────────────────────────────────────── -class TestSubstructureInvariants: +class TestSubstructureInvariants: def test_every_molecule_contains_itself(self): for smi in REFERENCE_SMILES: m = ro.parse_smiles(smi) - assert m.has_substruct_match(m), \ - f"Molecule does not contain itself: {smi}" + assert m.has_substruct_match(m), f"Molecule does not contain itself: {smi}" def test_every_molecule_contains_empty_query(self): query = ro.RustMolecule() for smi in REFERENCE_SMILES: m = ro.parse_smiles(smi) - assert m.has_substruct_match(query), \ - f"Empty query not matched by {smi}" + assert m.has_substruct_match(query), f"Empty query not matched by {smi}" def test_empty_contains_empty(self): assert ro.RustMolecule().has_substruct_match(ro.RustMolecule()) is True def test_empty_does_not_contain_real_molecule(self): empty = ro.RustMolecule() - real = ro.parse_smiles("CCO") + real = ro.parse_smiles("CCO") assert empty.has_substruct_match(real) is False def test_substructure_transitivity(self): """If A ⊆ B and B ⊆ C, then A ⊆ C.""" - a = ro.parse_smiles("CO") # methanol - b = ro.parse_smiles("CCO") # ethanol - c = ro.parse_smiles("CCCO") # propanol + a = ro.parse_smiles("CO") # methanol + b = ro.parse_smiles("CCO") # ethanol + c = ro.parse_smiles("CCCO") # propanol assert b.has_substruct_match(a) assert c.has_substruct_match(b) assert c.has_substruct_match(a) def test_asymmetry_non_substructure(self): """If A ⊄ B it does not follow that B ⊄ A (both can be true).""" - big = ro.parse_smiles("c1ccccc1") # benzene - small = ro.parse_smiles("CC") # ethane + big = ro.parse_smiles("c1ccccc1") # benzene + small = ro.parse_smiles("CC") # ethane # ethane is NOT a substructure of benzene (no saturated C-C single in benzene) assert not big.has_substruct_match(small) # ─── Coordinate invariants ──────────────────────────────────────────────────── -class TestCoordinateInvariants: +class TestCoordinateInvariants: def test_2d_coord_count_equals_atom_count(self): for smi in REFERENCE_SMILES: m = ro.generate_2d_coords(ro.parse_smiles(smi)) - assert len(m.coords_2d) == m.num_atoms, \ - f"2D coord count != atom count for {smi}" + assert len(m.coords_2d) == m.num_atoms, f"2D coord count != atom count for {smi}" def test_3d_coord_count_equals_atom_count(self): for smi in REFERENCE_SMILES: m = ro.generate_3d_coords(ro.parse_smiles(smi)) - assert len(m.coords_3d) == m.num_atoms, \ - f"3D coord count != atom count for {smi}" + assert len(m.coords_3d) == m.num_atoms, f"3D coord count != atom count for {smi}" def test_2d_coord_dimension_is_2(self): m = ro.generate_2d_coords(ro.parse_smiles("CCO")) @@ -250,8 +253,7 @@ def dist(a, b): for bond_idx in range(m.num_bonds): bond = m.get_bond(bond_idx) d = dist(c[bond.source_idx], c[bond.target_idx]) - assert 0.5 <= d <= 4.0, \ - f"Unreasonable bond length {d:.2f} Å for bond {bond_idx}" + assert 0.5 <= d <= 4.0, f"Unreasonable bond length {d:.2f} Å for bond {bond_idx}" def test_no_two_atoms_same_3d_position(self): """No two atoms should be at exactly the same 3D position.""" @@ -265,14 +267,13 @@ def dist_sq(a, b): for i in range(n): for j in range(i + 1, n): d2 = dist_sq(c[i], c[j]) - assert d2 > 1e-6, \ - f"Atoms {i} and {j} at same position: {c[i]}" + assert d2 > 1e-6, f"Atoms {i} and {j} at same position: {c[i]}" # ─── AMW ordering invariants ───────────────────────────────────────────────── -class TestAmwOrdering: +class TestAmwOrdering: def test_methane_lighter_than_ethane(self): assert ro.parse_smiles("C").amw < ro.parse_smiles("CC").amw @@ -281,12 +282,9 @@ def test_benzene_lighter_than_naphthalene(self): def test_adding_heavy_atom_increases_amw(self): """Adding a bromine (heavy) should increase AMW more than adding C.""" - base = ro.parse_smiles("CC") # ethane ~24 (heavy atoms only) - plus_c = ro.parse_smiles("CCC") # + one C - plus_br = ro.parse_smiles("CCBr") # + one Br - delta_c = plus_c.amw - base.amw + base = ro.parse_smiles("CC") # ethane ~24 (heavy atoms only) + plus_c = ro.parse_smiles("CCC") # + one C + plus_br = ro.parse_smiles("CCBr") # + one Br + delta_c = plus_c.amw - base.amw delta_br = plus_br.amw - base.amw assert delta_br > delta_c # Br (80) >> C (12) - - - diff --git a/tests/test_known_molecules.py b/tests/test_known_molecules.py index 1ad68db..7a445a0 100644 --- a/tests/test_known_molecules.py +++ b/tests/test_known_molecules.py @@ -16,47 +16,49 @@ • Aniline Nc1ccccc1 MW 93.13 • Imidazole c1cn[nH]c1 (or c1cnc[nH]1) MW 68.08 """ -import math -import pytest + import chem_engine as ro + # ─── molecule registry ────────────────────────────────────────────────────── MOLS = { - "aspirin": "CC(=O)Oc1ccccc1C(=O)O", - "paracetamol": "CC(=O)Nc1ccc(O)cc1", - "ibuprofen": "CC(C)Cc1ccc(cc1)C(C)C(=O)O", - "caffeine": "Cn1cnc2c1c(=O)n(C)c(=O)n2C", - "dopamine": "NCCc1ccc(O)c(O)c1", + "aspirin": "CC(=O)Oc1ccccc1C(=O)O", + "paracetamol": "CC(=O)Nc1ccc(O)cc1", + "ibuprofen": "CC(C)Cc1ccc(cc1)C(C)C(=O)O", + "caffeine": "Cn1cnc2c1c(=O)n(C)c(=O)n2C", + "dopamine": "NCCc1ccc(O)c(O)c1", "benzoic_acid": "OC(=O)c1ccccc1", - "naphthalene": "c1ccc2ccccc2c1", - "aniline": "Nc1ccccc1", - "benzene": "c1ccccc1", - "pyridine": "c1ccncc1", - "imidazole": "c1cnc[nH]1", - "ethanol": "CCO", - "propan1ol": "CCCO", - "butane": "CCCC", - "pentane": "CCCCC", - "hexane": "CCCCCC", - "cyclohexane": "C1CCCCC1", - "toluene": "Cc1ccccc1", - "phenol": "Oc1ccccc1", - "acetone": "CC(=O)C", - "acetic_acid": "CC(=O)O", - "ethylamine": "CCN", - "dimethylamine":"CNC", - "trimethylamine":"CN(C)C", + "naphthalene": "c1ccc2ccccc2c1", + "aniline": "Nc1ccccc1", + "benzene": "c1ccccc1", + "pyridine": "c1ccncc1", + "imidazole": "c1cnc[nH]1", + "ethanol": "CCO", + "propan1ol": "CCCO", + "butane": "CCCC", + "pentane": "CCCCC", + "hexane": "CCCCCC", + "cyclohexane": "C1CCCCC1", + "toluene": "Cc1ccccc1", + "phenol": "Oc1ccccc1", + "acetone": "CC(=O)C", + "acetic_acid": "CC(=O)O", + "ethylamine": "CCN", + "dimethylamine": "CNC", + "trimethylamine": "CN(C)C", "formaldehyde": "C=O", - "furan": "c1ccoc1", - "thiophene": "c1ccsc1", - "pyrrole": "c1cc[nH]c1", + "furan": "c1ccoc1", + "thiophene": "c1ccsc1", + "pyrrole": "c1cc[nH]c1", } + def parse(name: str) -> ro.RustMolecule: return ro.parse_smiles(MOLS[name]) # ─── SMILES parsing - heavy-atom counts ───────────────────────────────────── + class TestAtomCounts: """Verify heavy atom and bond counts for reference molecules.""" @@ -83,8 +85,7 @@ def test_pyridine_6_atoms(self): def test_pyridine_1_nitrogen(self): m = parse("pyridine") - n_count = sum(1 for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 7) + n_count = sum(1 for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 7) assert n_count == 1 def test_aniline_7_atoms(self): @@ -140,44 +141,37 @@ class TestAtomTypes: def test_aspirin_carbon_count(self): m = parse("aspirin") - c_count = sum(1 for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 6) + c_count = sum(1 for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 6) assert c_count == 9 def test_aspirin_oxygen_count(self): m = parse("aspirin") - o_count = sum(1 for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 8) + o_count = sum(1 for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 8) assert o_count == 4 def test_caffeine_nitrogen_count(self): m = parse("caffeine") - n_count = sum(1 for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 7) + n_count = sum(1 for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 7) assert n_count == 4 def test_caffeine_oxygen_count(self): m = parse("caffeine") - o_count = sum(1 for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 8) + o_count = sum(1 for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 8) assert o_count == 2 def test_thiophene_has_sulfur(self): m = parse("thiophene") - s_count = sum(1 for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 16) + s_count = sum(1 for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 16) assert s_count == 1 def test_pyrrole_has_nitrogen(self): m = parse("pyrrole") - n_count = sum(1 for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 7) + n_count = sum(1 for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 7) assert n_count == 1 def test_furan_has_oxygen(self): m = parse("furan") - o_count = sum(1 for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 8) + o_count = sum(1 for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 8) assert o_count == 1 def test_benzene_all_aromatic(self): @@ -199,8 +193,7 @@ def test_toluene_ring_aromatic_methyl_not(self): def test_aniline_nitrogen_not_aromatic(self): """Aniline's NH2 nitrogen is exocyclic and not aromatic.""" m = parse("aniline") - n_atoms = [m.get_atom(i) for i in range(m.num_atoms) - if m.get_atom(i).atomic_number == 7] + n_atoms = [m.get_atom(i) for i in range(m.num_atoms) if m.get_atom(i).atomic_number == 7] assert len(n_atoms) == 1 assert not n_atoms[0].is_aromatic @@ -354,39 +347,57 @@ class TestBondTypes: def test_formaldehyde_double_bond(self): m = ro.parse_smiles("C=O") - double_bonds = [m.get_bond(i) for i in range(m.num_bonds) - if m.get_bond(i).bond_type == ro.BondType.Double] + double_bonds = [ + m.get_bond(i) + for i in range(m.num_bonds) + if m.get_bond(i).bond_type == ro.BondType.Double + ] assert len(double_bonds) == 1 def test_acetonitrile_triple_bond(self): m = ro.parse_smiles("CC#N") - triple_bonds = [m.get_bond(i) for i in range(m.num_bonds) - if m.get_bond(i).bond_type == ro.BondType.Triple] + triple_bonds = [ + m.get_bond(i) + for i in range(m.num_bonds) + if m.get_bond(i).bond_type == ro.BondType.Triple + ] assert len(triple_bonds) == 1 def test_butadiene_two_double_bonds(self): m = ro.parse_smiles("C=CC=C") - double_bonds = [m.get_bond(i) for i in range(m.num_bonds) - if m.get_bond(i).bond_type == ro.BondType.Double] + double_bonds = [ + m.get_bond(i) + for i in range(m.num_bonds) + if m.get_bond(i).bond_type == ro.BondType.Double + ] assert len(double_bonds) == 2 def test_carboxyl_double_bond(self): m = ro.parse_smiles("CC(=O)O") # acetic acid - double_bonds = [m.get_bond(i) for i in range(m.num_bonds) - if m.get_bond(i).bond_type == ro.BondType.Double] + double_bonds = [ + m.get_bond(i) + for i in range(m.num_bonds) + if m.get_bond(i).bond_type == ro.BondType.Double + ] assert len(double_bonds) == 1 def test_benzene_aromatic_bonds(self): """chem-engine now correctly stores aromatic-ring bonds as BondType.Aromatic.""" m = parse("benzene") - aromatic_bonds = [m.get_bond(i) for i in range(m.num_bonds) - if m.get_bond(i).bond_type == ro.BondType.Aromatic] + aromatic_bonds = [ + m.get_bond(i) + for i in range(m.num_bonds) + if m.get_bond(i).bond_type == ro.BondType.Aromatic + ] assert len(aromatic_bonds) == 6 def test_pyridine_aromatic_bonds(self): m = parse("pyridine") - aromatic_bonds = [m.get_bond(i) for i in range(m.num_bonds) - if m.get_bond(i).bond_type == ro.BondType.Aromatic] + aromatic_bonds = [ + m.get_bond(i) + for i in range(m.num_bonds) + if m.get_bond(i).bond_type == ro.BondType.Aromatic + ] assert len(aromatic_bonds) == 6 @@ -407,29 +418,23 @@ def test_canonical_atom_count_preserved(self): orig = ro.parse_smiles(smi) can = ro.canonicalize(orig) round_trip = ro.parse_smiles(can) - assert round_trip.num_atoms == orig.num_atoms, \ + assert round_trip.num_atoms == orig.num_atoms, ( f"Atom count mismatch for {name}: {orig.num_atoms} → {round_trip.num_atoms}" + ) def test_canonical_bond_count_preserved(self): for name, smi in MOLS.items(): orig = ro.parse_smiles(smi) can = ro.canonicalize(orig) round_trip = ro.parse_smiles(can) - assert round_trip.num_bonds == orig.num_bonds, \ - f"Bond count mismatch for {name}" + assert round_trip.num_bonds == orig.num_bonds, f"Bond count mismatch for {name}" def test_canonical_benzene_toluene_differ(self): c_benz = ro.canonicalize(parse("benzene")) - c_tol = ro.canonicalize(parse("toluene")) + c_tol = ro.canonicalize(parse("toluene")) assert c_benz != c_tol def test_canonical_ethanol_propanol_differ(self): c1 = ro.canonicalize(parse("ethanol")) c2 = ro.canonicalize(parse("propan1ol")) assert c1 != c2 - - - - - - diff --git a/tests/test_substructure_extended.py b/tests/test_substructure_extended.py index bf628be..f71d519 100644 --- a/tests/test_substructure_extended.py +++ b/tests/test_substructure_extended.py @@ -13,12 +13,13 @@ • Chained matches (transitivity chains) • Combinatorial query × target sweep """ + import pytest + import chem_engine as ro class TestBasicSubstructure: - def test_single_carbon_in_all_organics(self): # Note: [C] is an aliphatic carbon. chem-engine's substructure search # checks is_aromatic flag, so [C] (aliphatic) does NOT match aromatic c @@ -28,12 +29,12 @@ def test_single_carbon_in_all_organics(self): aromatic_targets = ["c1ccccc1"] # aromatic - [C] should NOT match for smi in aliphatic_targets: target = ro.parse_smiles(smi) - assert target.has_substruct_match(query), \ - f"[C] not found in {smi}" + assert target.has_substruct_match(query), f"[C] not found in {smi}" for smi in aromatic_targets: target = ro.parse_smiles(smi) - assert not target.has_substruct_match(query), \ + assert not target.has_substruct_match(query), ( f"[C] (aliphatic) should not match aromatic carbons in {smi}" + ) def test_single_oxygen_in_oxygen_containing(self): query = ro.parse_smiles("[O]") @@ -48,34 +49,34 @@ def test_single_nitrogen_not_in_hydrocarbons(self): def test_methanol_in_longer_alcohols(self): query = ro.parse_smiles("CO") targets_yes = ["CCO", "CCCO", "CCCCO", "CC(O)C"] - targets_no = ["CC", "c1ccccc1", "CCN"] + targets_no = ["CC", "c1ccccc1", "CCN"] for smi in targets_yes: - assert ro.parse_smiles(smi).has_substruct_match(query), \ - f"CO not found in {smi}" + assert ro.parse_smiles(smi).has_substruct_match(query), f"CO not found in {smi}" for smi in targets_no: - assert not ro.parse_smiles(smi).has_substruct_match(query), \ - f"CO wrongly found in {smi}" + assert not ro.parse_smiles(smi).has_substruct_match(query), f"CO wrongly found in {smi}" def test_benzene_ring_in_aromatic_compounds(self): query = ro.parse_smiles("c1ccccc1") positives = [ - "c1ccccc1", # benzene itself - "Cc1ccccc1", # toluene - "Oc1ccccc1", # phenol - "Nc1ccccc1", # aniline - "CC(=O)Oc1ccccc1C(=O)O",# aspirin + "c1ccccc1", # benzene itself + "Cc1ccccc1", # toluene + "Oc1ccccc1", # phenol + "Nc1ccccc1", # aniline + "CC(=O)Oc1ccccc1C(=O)O", # aspirin ] negatives = [ - "C1CCCCC1", # cyclohexane (saturated) - "CCO", # ethanol - "CCCC", # butane + "C1CCCCC1", # cyclohexane (saturated) + "CCO", # ethanol + "CCCC", # butane ] for smi in positives: - assert ro.parse_smiles(smi).has_substruct_match(query), \ + assert ro.parse_smiles(smi).has_substruct_match(query), ( f"Benzene query not found in {smi}" + ) for smi in negatives: - assert not ro.parse_smiles(smi).has_substruct_match(query), \ + assert not ro.parse_smiles(smi).has_substruct_match(query), ( f"Benzene query wrongly found in {smi}" + ) def test_carbonyl_in_carbonyl_compounds(self): query = ro.parse_smiles("C=O") @@ -111,7 +112,6 @@ def test_aromatic_not_matched_by_aliphatic_ring(self): class TestSubstructureWithHeteroatoms: - def test_pyridine_ring_in_pyridine_derivatives(self): query = ro.parse_smiles("c1ccncc1") # pyridine assert ro.parse_smiles("c1ccncc1").has_substruct_match(query) @@ -133,21 +133,18 @@ def test_carboxyl_group(self): query = ro.parse_smiles("C(=O)O") # carboxyl fragment positives = ["CC(=O)O", "OC(=O)c1ccccc1", "CC(=O)Oc1ccccc1C(=O)O"] for smi in positives: - assert ro.parse_smiles(smi).has_substruct_match(query), \ - f"Carboxyl not found in {smi}" + assert ro.parse_smiles(smi).has_substruct_match(query), f"Carboxyl not found in {smi}" def test_amide_fragment(self): query = ro.parse_smiles("C(=O)N") # amide bond positives = ["CC(=O)N", "CC(=O)NC", "CC(=O)Nc1ccc(O)cc1"] for smi in positives: - assert ro.parse_smiles(smi).has_substruct_match(query), \ - f"Amide not found in {smi}" + assert ro.parse_smiles(smi).has_substruct_match(query), f"Amide not found in {smi}" # ester (C(=O)O) should not match amide (C(=O)N) assert not ro.parse_smiles("CC(=O)O").has_substruct_match(query) class TestSubstructureLargerMolecules: - def test_aspirin_contains_benzene(self): aspirin = ro.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") benzene = ro.parse_smiles("c1ccccc1") @@ -155,7 +152,7 @@ def test_aspirin_contains_benzene(self): def test_aspirin_contains_ester(self): aspirin = ro.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") - ester = ro.parse_smiles("CC(=O)O") + ester = ro.parse_smiles("CC(=O)O") assert aspirin.has_substruct_match(ester) def test_ibuprofen_contains_benzene(self): @@ -167,19 +164,18 @@ def test_caffeine_contains_imidazole_fragment(self): """Caffeine contains a fused purine ring system with aromatic N-heterocycles.""" caffeine = ro.parse_smiles("Cn1cnc2c1c(=O)n(C)c(=O)n2C") # Check that caffeine has nitrogen atoms (atomic_number == 7) - n_count = sum(1 for i in range(caffeine.num_atoms) - if caffeine.get_atom(i).atomic_number == 7) + n_count = sum( + 1 for i in range(caffeine.num_atoms) if caffeine.get_atom(i).atomic_number == 7 + ) assert n_count == 4 # caffeine has 4 N atoms # Aromatic N query: use bracket [n] (aromatic N, is_aromatic=True) # chem-engine match: [n] should match aromatic N atoms in caffeine - n_arom_query = ro.parse_smiles("[nH]") # aromatic NH in pyrrole # Just confirm we can query; exact match depends on parser flag assert caffeine.num_atoms == 14 def test_dopamine_contains_catechol(self): """Dopamine contains a catechol (1,2-dihydroxybenzene) motif.""" dopamine = ro.parse_smiles("NCCc1ccc(O)c(O)c1") - catechol = ro.parse_smiles("Oc1ccccc1O") # simplified # Note: chem-engine does exact atom matching; test that O-containing # aromatic ring fragment is found phenol_query = ro.parse_smiles("Oc1ccccc1") @@ -199,12 +195,18 @@ def test_naphthalene_does_not_contain_pyridine(self): class TestSubstructureSelfContainment: """Every molecule must contain itself (identity substructure).""" - @pytest.mark.parametrize("smi", [ - "C", "CCO", "c1ccccc1", "C1CCCCC1", - "CC(=O)Oc1ccccc1C(=O)O", - "Cn1cnc2c1c(=O)n(C)c(=O)n2C", - "CC(C)Cc1ccc(cc1)C(C)C(=O)O", - ]) + @pytest.mark.parametrize( + "smi", + [ + "C", + "CCO", + "c1ccccc1", + "C1CCCCC1", + "CC(=O)Oc1ccccc1C(=O)O", + "Cn1cnc2c1c(=O)n(C)c(=O)n2C", + "CC(C)Cc1ccc(cc1)C(C)C(=O)O", + ], + ) def test_self_match(self, smi): m = ro.parse_smiles(smi) assert m.has_substruct_match(m) @@ -220,12 +222,7 @@ def test_single_S_in_thioether(self): assert ro.parse_smiles("CSC").has_substruct_match(ro.parse_smiles("[S]")) def test_single_F_in_fluorobenzene(self): - assert ro.parse_smiles("Fc1ccccc1").has_substruct_match( - ro.parse_smiles("[F]")) + assert ro.parse_smiles("Fc1ccccc1").has_substruct_match(ro.parse_smiles("[F]")) def test_single_Cl_not_in_fluorobenzene(self): - assert not ro.parse_smiles("Fc1ccccc1").has_substruct_match( - ro.parse_smiles("[Cl]")) - - - + assert not ro.parse_smiles("Fc1ccccc1").has_substruct_match(ro.parse_smiles("[Cl]")) diff --git a/tests/test_tautomers_extended.py b/tests/test_tautomers_extended.py index 3560ba4..705732f 100644 --- a/tests/test_tautomers_extended.py +++ b/tests/test_tautomers_extended.py @@ -13,12 +13,13 @@ • Canonical tautomer has correct atom count • Canonical tautomer scoring (keto wins over enol) """ + import pytest + import chem_engine as ro class TestTautomerEnumeration: - def test_acetone_keto_enol(self): """Acetone CC(=O)C → keto + enol = ≥ 2 tautomers.""" m = ro.parse_smiles("CC(=O)C") @@ -48,7 +49,7 @@ def test_ethanol_no_keto_enol(self): """Ethanol CCO has no C=O so no keto-enol tautomer applies.""" m = ro.parse_smiles("CCO") t = m.enumerate_tautomers() - assert len(t) >= 1 # original always returned + assert len(t) >= 1 # original always returned def test_benzene_no_tautomers(self): """Aromatic benzene - no tautomeric shifts applicable.""" @@ -78,7 +79,7 @@ def test_beta_ketoester_tautomers(self): """Methyl acetoacetate CC(=O)CC(=O)OC - two carbonyl groups.""" m = ro.parse_smiles("CC(=O)CC(=O)OC") t = m.enumerate_tautomers() - assert len(t) >= 1 # at minimum original + assert len(t) >= 1 # at minimum original def test_malonaldehyde_tautomers(self): """Malonaldehyde O=CCC=O - two aldehyde groups.""" @@ -107,7 +108,6 @@ def test_aspirin_tautomers_no_crash(self): class TestCanonicalTautomer: - def test_canonical_is_rustmolecule(self): m = ro.parse_smiles("CC(=O)C") c = m.get_canonical_tautomer() @@ -129,9 +129,11 @@ def test_canonical_keto_preference_acetone(self): c = m.get_canonical_tautomer() # Canonical tautomer should have at least one C=O bond has_c_double_o = any( - c.get_bond(i).bond_type == ro.BondType.Double and - (c.get_atom(c.get_bond(i).source_idx).atomic_number == 8 or - c.get_atom(c.get_bond(i).target_idx).atomic_number == 8) + c.get_bond(i).bond_type == ro.BondType.Double + and ( + c.get_atom(c.get_bond(i).source_idx).atomic_number == 8 + or c.get_atom(c.get_bond(i).target_idx).atomic_number == 8 + ) for i in range(c.num_bonds) ) assert has_c_double_o @@ -150,14 +152,19 @@ def test_canonical_of_simple_mol_is_self(self): assert c.num_atoms == m.num_atoms assert c.num_bonds == m.num_bonds - @pytest.mark.parametrize("smi", [ - "CC(=O)C", "CC=O", "CC(=O)N", "O=CCC=O", - "Cn1cnc2c1c(=O)n(C)c(=O)n2C", - "CC(=O)Oc1ccccc1C(=O)O", - ]) + @pytest.mark.parametrize( + "smi", + [ + "CC(=O)C", + "CC=O", + "CC(=O)N", + "O=CCC=O", + "Cn1cnc2c1c(=O)n(C)c(=O)n2C", + "CC(=O)Oc1ccccc1C(=O)O", + ], + ) def test_canonical_does_not_crash(self, smi): m = ro.parse_smiles(smi) c = m.get_canonical_tautomer() assert c is not None assert c.num_atoms > 0 - From 9dedeabaf7cf58f60847b10334d7be082b067d91 Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:02:39 +0200 Subject: [PATCH 03/12] fix: use uv run pytest in pre-commit hooks (no .venv required) --- .pre-commit-config.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6b4225..bfcb504 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - id: pytest-testmon name: "pytest-testmon: run only affected tests" language: system - entry: .venv/bin/pytest tests/ --testmon --ignore=tests/test_correctness_vs_rdkit.py + entry: uv run pytest tests/ --testmon --ignore=tests/test_correctness_vs_rdkit.py pass_filenames: false types: [python] stages: [pre-commit] @@ -52,9 +52,7 @@ repos: - id: pytest-xdist-full name: "pytest-xdist: full test suite in parallel (pre-push)" language: system - entry: .venv/bin/pytest tests/ -n auto --ignore=tests/test_correctness_vs_rdkit.py + entry: uv run pytest tests/ -n auto --ignore=tests/test_correctness_vs_rdkit.py pass_filenames: false types: [python] stages: [pre-push] - - From 9ca8187db7ce064589fa49aa4c1bda6a6d9ea7dc Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:07:01 +0200 Subject: [PATCH 04/12] fix: bump requires-python to >=3.10 (pytest-testmon needs 3.10+) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dc7a406..d78a9b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "chem-engine" -requires-python = ">=3.8" +requires-python = ">=3.10" classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", @@ -43,7 +43,7 @@ show_missing = true # ── ruff ────────────────────────────────────────────────────────────────────── [tool.ruff] line-length = 100 -target-version = "py38" +target-version = "py310" [tool.ruff.lint] select = ["E", "F", "W", "I", "UP"] From f0e1b4d3cc8af0a5b9d2a4fa60336c35468c55c0 Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:08:10 +0200 Subject: [PATCH 05/12] Fix files not compliant with pre-commit hooks --- .editorconfig | 1 - .github/workflows/ci.yml | 2 -- .gitignore | 2 -- .secrets.baseline | 1 - README.md | 2 -- benchmarks/extreme_scale_benchmark.py | 2 +- docs/BENCHMARKS.md | 19 +++++++++---------- docs/FEATURES.md | 7 +++---- docs/index.md | 1 - src/algorithms/layout.rs | 12 ++++++------ src/lib.rs | 4 ++-- src/molecule.rs | 14 +++++++------- 12 files changed, 28 insertions(+), 39 deletions(-) diff --git a/.editorconfig b/.editorconfig index 338ea0b..9db580b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,4 +16,3 @@ indent_size = 4 [*.md] trim_trailing_whitespace = false - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f55c849..704b1c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,5 +83,3 @@ jobs: - name: Run RDKit cross-validation tests run: uv run pytest tests/test_correctness_vs_rdkit.py --tb=short -q - - diff --git a/.gitignore b/.gitignore index ce57816..65c81b2 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,3 @@ dist/ *.egg-info/*.iml docs/specs - - diff --git a/.secrets.baseline b/.secrets.baseline index 81eca94..d877874 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1,2 +1 @@ {"version": "1.5.0", "plugins_used": [], "filters_used": [], "results": {}, "generated_at": ""} - diff --git a/README.md b/README.md index 744feba..52dd972 100644 --- a/README.md +++ b/README.md @@ -251,5 +251,3 @@ See [docs/FEATURES.md#limitations](docs/FEATURES.md#13-limitations-and-known-gap ## License [MIT](LICENSE) - Copyright (c) 2026 Vandan Revanur - - diff --git a/benchmarks/extreme_scale_benchmark.py b/benchmarks/extreme_scale_benchmark.py index c5d99af..85909d5 100644 --- a/benchmarks/extreme_scale_benchmark.py +++ b/benchmarks/extreme_scale_benchmark.py @@ -77,9 +77,9 @@ import threading import time import warnings +from collections.abc import Iterator from concurrent.futures import Future, ProcessPoolExecutor, as_completed from pathlib import Path -from typing import Iterator # ── process priority ───────────────────────────────────────────────────────── try: diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 8574f67..9727bb7 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -1,9 +1,9 @@ # chem-engine - Benchmarking Summary -> **Platform:** Intel Core i7-11850H @ 2.50 GHz, 16 threads, 33 GB RAM, Linux x86_64 -> **chem-engine:** v0.1.0 (`maturin develop --release`) -> **RDKit:** 2026.03.2 -> **Date:** July 31, 2026 +> **Platform:** Intel Core i7-11850H @ 2.50 GHz, 16 threads, 33 GB RAM, Linux x86_64 +> **chem-engine:** v0.1.0 (`maturin develop --release`) +> **RDKit:** 2026.03.2 +> **Date:** July 31, 2026 > **Test suite:** 481 tests, 0 failures --- @@ -24,15 +24,15 @@ | Substructure search | 0.005 ms | 0.013 ms | **2.4×** | | Batch parse - 1 000 mols (Rayon) | 0.69 ms | 9.55 ms | **13.8×** | -† Tautomer speedup reflects reduced rule coverage (keto-enol only vs RDKit's 36-rule set). +† Tautomer speedup reflects reduced rule coverage (keto-enol only vs RDKit's 36-rule set). ❌ Tanimoto is slower due to `Vec` fingerprint storage (no SIMD popcount); fix pending. --- ## 2. Large-scale parallel benchmark - ChEMBL 37, up to 50 K molecules -Harness: `benchmarks/large_scale_benchmark.py` -Configuration: 4 workers (`cpu_count // 2`), `ProcessPoolExecutor`, `os.nice(10)`, 50 ms inter-chunk sleep +Harness: `benchmarks/large_scale_benchmark.py` +Configuration: 4 workers (`cpu_count // 2`), `ProcessPoolExecutor`, `os.nice(10)`, 50 ms inter-chunk sleep Numbers include IPC overhead (SMILES pickled across process boundary). ### SMILES Parsing @@ -89,8 +89,8 @@ Numbers include IPC overhead (SMILES pickled across process boundary). ## 3. Extreme-scale streaming benchmark - ChEMBL 37 cycled / multi-DB, up to 10 M molecules -Harness: `benchmarks/extreme_scale_benchmark.py` -Configuration: 5 workers (`cpu_count // 3`), streaming 200 K validated pool cycled to reach 1 M/10 M, +Harness: `benchmarks/extreme_scale_benchmark.py` +Configuration: 5 workers (`cpu_count // 3`), streaming 200 K validated pool cycled to reach 1 M/10 M, memory watchdog (psutil, 70% RAM limit), 10-min time budget per operation, `os.nice(10)` > Numbers at 1 M are **measured**; 10 M are **projected** from measured trends (±5%). @@ -214,4 +214,3 @@ python benchmarks/extreme_scale_benchmark.py \ # Full test suite (481 tests) python -m pytest tests/ -q ``` - diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 344967f..773d842 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -220,7 +220,7 @@ mol = ce.generate_3d_coords(mol) coords = mol.coords_3d # list of [x, y, z] for each heavy atom ``` -**Speedup vs RDKit `EmbedMolecule` (ETKDG):** ~37x +**Speedup vs RDKit `EmbedMolecule` (ETKDG):** ~37x **Note:** Operates on heavy atoms only (no explicit H). For full stereochemical accuracy, use RDKit's ETKDG after converting via `to_rdkit()`. @@ -237,7 +237,7 @@ mol = ce.parse_smiles("[H]O[H]") print(mol.amw) # 18.015 ``` -AMW = sum of heavy-atom masses + explicit H masses (1.008 Da per H). +AMW = sum of heavy-atom masses + explicit H masses (1.008 Da per H). **Important:** Implicit hydrogens on organic-subset atoms (e.g., `C` in `CCO`) are not counted unless written explicitly as bracket atoms (`[CH4]`). Use `[H]O[H]` form or convert via RDKit for full-precision MW. @@ -445,7 +445,7 @@ The organic subset (without brackets) supports: B, C, N, O, F, P, S, Cl, Br, I. ### Organic subset (no brackets needed) -`B` `C` `N` `O` `F` `P` `S` `Cl` `Br` `I` `H` +`B` `C` `N` `O` `F` `P` `S` `Cl` `Br` `I` `H` Aromatic variants: `b` `c` `n` `o` `p` `s` (and `as`, `se`) ### Full element table (bracket notation `[Na+]`, `[Fe]`, etc.) @@ -475,4 +475,3 @@ Aromatic variants: `b` `c` `n` `o` `p` `s` (and `as`, `se`) | **Macromolecules** | No support for BIOVIA extended SMILES or HELM | Out of scope for v0.1 | | **Reaction SMILES** | Not supported | Out of scope | | **SMARTS queries** | `has_substruct_match` uses SMILES queries, not SMARTS patterns | Add SMARTS parser | - diff --git a/docs/index.md b/docs/index.md index 9babf22..689f067 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,4 +6,3 @@ | [BENCHMARKS.md](BENCHMARKS.md) | Benchmark methodology and throughput tables (micro / 50K / 10M scale) | For a general overview see the [project README](../README.md). - diff --git a/src/algorithms/layout.rs b/src/algorithms/layout.rs index f6de786..038f27a 100644 --- a/src/algorithms/layout.rs +++ b/src/algorithms/layout.rs @@ -32,12 +32,12 @@ pub fn generate_2d_coords(mol: &mut RustMolecule) { let dy = coords[i][1] - coords[j][1]; let dist_sq = dx * dx + dy * dy + 1e-4; let dist = dist_sq.sqrt(); - + if dist < 5.0 { let force = k_repulsion / dist_sq; let fx = (dx / dist) * force; let fy = (dy / dist) * force; - + forces[i][0] += fx; forces[i][1] += fy; forces[j][0] -= fx; @@ -56,7 +56,7 @@ pub fn generate_2d_coords(mol: &mut RustMolecule) { let dx = coords[u][0] - coords[v][0]; let dy = coords[u][1] - coords[v][1]; let dist = (dx * dx + dy * dy + 1e-4).sqrt(); - + let force = k_spring * (dist - d_zero); let fx = (dx / dist) * force; let fy = (dy / dist) * force; @@ -89,7 +89,7 @@ pub fn generate_3d_coords(mol: &mut RustMolecule) { // Distance Geometry (ETKDG-like inflation) // 1. Generate Distance Bounds Matrix let mut d_matrix = vec![vec![0.0; num_atoms]; num_atoms]; - + // Build shortest path distances to approximate bounds let mut adj = vec![vec![1e9; num_atoms]; num_atoms]; for i in 0..num_atoms { @@ -179,10 +179,10 @@ pub fn generate_3d_coords(mol: &mut RustMolecule) { let dy = coords[i][1] - coords[j][1]; let dz = coords[i][2] - coords[j][2]; let dist = (dx * dx + dy * dy + dz * dz + 1e-4).sqrt(); - + let target_dist = d_matrix[i][j]; let force = k_bond * (dist - target_dist); - + let fx = (dx / dist) * force; let fy = (dy / dist) * force; let fz = (dz / dist) * force; diff --git a/src/lib.rs b/src/lib.rs index bb0c0aa..5edb838 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,12 +55,12 @@ fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - + m.add_function(wrap_pyfunction!(parse_smiles, m)?)?; m.add_function(wrap_pyfunction!(canonicalize, m)?)?; m.add_function(wrap_pyfunction!(generate_2d_coords, m)?)?; m.add_function(wrap_pyfunction!(generate_3d_coords, m)?)?; m.add_function(wrap_pyfunction!(batch_parse_smiles, m)?)?; - + Ok(()) } diff --git a/src/molecule.rs b/src/molecule.rs index 68f9094..bc15f78 100644 --- a/src/molecule.rs +++ b/src/molecule.rs @@ -193,7 +193,7 @@ impl RustMolecule { pub fn similarity(&self, other: &RustMolecule) -> f64 { let fp1 = self.get_fingerprint(); let fp2 = other.get_fingerprint(); - + let mut intersection = 0; let mut union = 0; for i in 0..2048 { @@ -204,7 +204,7 @@ impl RustMolecule { union += 1; } } - + if union == 0 { 0.0 } else { @@ -216,7 +216,7 @@ impl RustMolecule { pub fn enumerate_tautomers(&self) -> Vec { let mut tautomers = vec![self.clone()]; let num_atoms = self.inner.atoms.len(); - + // Rules engine to find keto-enol / amide-imidic tautomeric systems // Pattern: [O,N,S]=[C,N]-[C,N]-[H] (1-3 proton shift) for bond_idx in 0..self.inner.bonds.len() { @@ -268,7 +268,7 @@ impl RustMolecule { // Create tautomer molecule by shifting proton let mut t_mol = self.clone(); let mut t_data = (*t_mol.inner).clone(); - + // 1. Shift H from neighbor to heteroatom if t_data.atoms[c_neigh].num_explicit_hs > 0 { t_data.atoms[c_neigh].num_explicit_hs -= 1; @@ -356,7 +356,7 @@ impl RustMolecule { let num_atoms = self.inner.atoms.len(); let mut visited = vec![false; num_atoms]; let mut queue = std::collections::VecDeque::new(); - + queue.push_back(u); visited[u] = true; @@ -451,14 +451,14 @@ impl RustMolecule { fn score_tautomer(mol: &RustMolecule) -> i32 { let mut score = 0; - + // Keto form preferenced over enol form // Count double bonded oxygen C=O for bond in &mol.inner.bonds { if bond.bond_type == BondType::Double { let u_atom = &mol.inner.atoms[bond.source_idx]; let v_atom = &mol.inner.atoms[bond.target_idx]; - + if (u_atom.atomic_number == 8 && v_atom.atomic_number == 6) || (v_atom.atomic_number == 8 && u_atom.atomic_number == 6) { score += 15; From 81c3f838c279b0065923e2f290421cc2aa2748d1 Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:12:29 +0200 Subject: [PATCH 06/12] Add uv.lock --- uv.lock | 554 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 uv.lock diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..4232889 --- /dev/null +++ b/uv.lock @@ -0,0 +1,554 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + +[[package]] +name = "chem-engine" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-testmon" }, + { name = "pytest-xdist" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.0" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-cov", specifier = ">=5.0" }, + { name = "pytest-testmon", specifier = ">=2.2.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "ruff", specifier = ">=0.11" }, +] + +[[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 = "coverage" +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/060ce69008ac97bbc01b1411b3e55b61f6f015659400b46749b662107831/coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d", size = 221284, upload-time = "2026-07-15T18:53:29.52Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a3/d936e8b53edd9684100a6aefaf3fcabaa54728fe33324436c8d279c047aa/coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846", size = 221799, upload-time = "2026-07-15T18:53:31.708Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a3/ca234b06aec7ee28226f11d39a696b4481fe5eddfce8e03bf39979bb8ffb/coverage-7.15.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf", size = 248544, upload-time = "2026-07-15T18:53:33.212Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/dda79527bb7573ba91828b2fb91b3105d87378d6a2749ca0c0924ce0addd/coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376", size = 250374, upload-time = "2026-07-15T18:53:34.683Z" }, + { url = "https://files.pythonhosted.org/packages/67/c6/c33755a34572f81f49a8c0cdf6b622f35ccb3238b136e1909daf0cdd4319/coverage-7.15.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd", size = 252239, upload-time = "2026-07-15T18:53:36.205Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6f/dc341741b375be53a5baeee5b4bf0f0e525d38caed428f7932d23bb7bcb1/coverage-7.15.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb", size = 254150, upload-time = "2026-07-15T18:53:37.863Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8d/966a18a5b195cb4e77b14c53f5f3dce22b5da05e6de7fafd1e08f2d2067a/coverage-7.15.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1", size = 249234, upload-time = "2026-07-15T18:53:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/8b2e367496ab48484d48e79984fec76cdc1b7cb5d3a00ee799a5602e3ec9/coverage-7.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9", size = 250276, upload-time = "2026-07-15T18:53:41.027Z" }, + { url = "https://files.pythonhosted.org/packages/63/92/1199318a200eb6c8c6ce0192c892c8710ac791abbe0f35099294620bbfda/coverage-7.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a", size = 248283, upload-time = "2026-07-15T18:53:42.557Z" }, + { url = "https://files.pythonhosted.org/packages/56/da/be284a55c5619bda891a89c27dfd59324a2c6a14d755cf6aac6960ceebeb/coverage-7.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287", size = 252093, upload-time = "2026-07-15T18:53:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/d4/53/ee112da833ddd77b73c6d781a98029b45b584b136615b4900ed0569f887e/coverage-7.15.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89", size = 248552, upload-time = "2026-07-15T18:53:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/82/6a/802cfc802e9113494c80bf3f284cd4d72faeb1f24e244f61046af364f2ca/coverage-7.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88", size = 249154, upload-time = "2026-07-15T18:53:47.256Z" }, + { url = "https://files.pythonhosted.org/packages/2c/65/529808e91d651147edae408fd9e894abc3b8cad7f3e594bbc36719a3e13a/coverage-7.15.2-cp310-cp310-win32.whl", hash = "sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443", size = 223334, upload-time = "2026-07-15T18:53:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/68/0f/0e1829d7001130876dfbc0b4e1c737ea7c155b809e3e4a98a0aa268e2369/coverage-7.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629", size = 223959, upload-time = "2026-07-15T18:53:50.429Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3a/54536704f507d4573bf9161c4d0dd3dd59b6d85e48c664e901b6844d8e33/coverage-7.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036", size = 221414, upload-time = "2026-07-15T18:53:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d9/8ba925d29743e3577b21e4d8c11a702b76bc93c41e7fdfd1177af63d4b8d/coverage-7.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660", size = 221913, upload-time = "2026-07-15T18:53:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/a855f3aa0187f2b431ade4e4791b77b56282cfb5d201c83ec26a31b5b36a/coverage-7.15.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589", size = 252332, upload-time = "2026-07-15T18:53:55.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d3/13ac97b4370640ba3452fc8559b06cc2f479ce3ba4a0b632a73e44c38a7d/coverage-7.15.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee", size = 254243, upload-time = "2026-07-15T18:53:57.055Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/5eca144942d8d0659d3f55176517f4a59cdc65eefd17146a0770935a3ebd/coverage-7.15.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0", size = 256352, upload-time = "2026-07-15T18:53:58.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ba/d3db2e01a50fc88cdb4c0f19542bcf6f61489e34dc9aa3538413e2459a38/coverage-7.15.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487", size = 258313, upload-time = "2026-07-15T18:54:00.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/b3/aba83416e9177df28e5186d856c19158c59fc0e7e814aaa61a4a2354ad1b/coverage-7.15.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f", size = 252449, upload-time = "2026-07-15T18:54:02.456Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a5/4b00ecac0194431ab451b0f6710f8e2517d04cef60f821b14dec4637d575/coverage-7.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1", size = 254043, upload-time = "2026-07-15T18:54:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/75/b6/cfa209b4313ee7f1b34da47efcd789ea51c024ad35af390e00f5a3c10a2e/coverage-7.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5", size = 252107, upload-time = "2026-07-15T18:54:06.745Z" }, + { url = "https://files.pythonhosted.org/packages/36/67/e8cac5a6954038c98d7fe7eb9802afe7ab3ecb637bb7cc00e69b4148b56d/coverage-7.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0", size = 255873, upload-time = "2026-07-15T18:54:08.48Z" }, + { url = "https://files.pythonhosted.org/packages/2c/92/395cca9f330a86c3fe3471d73e2c102116c4c58fdc619dbbc125c6e93a54/coverage-7.15.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad", size = 251826, upload-time = "2026-07-15T18:54:10.083Z" }, + { url = "https://files.pythonhosted.org/packages/51/60/3e91b20295439652424f426b7086ec5bf4fbe3f604c73eda22b986c4fd6b/coverage-7.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db", size = 252735, upload-time = "2026-07-15T18:54:11.878Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/8c07839005e5e3c6b3877d3a6e2a80ce766589f31dd2b6882b78d59a7b8c/coverage-7.15.2-cp311-cp311-win32.whl", hash = "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9", size = 223500, upload-time = "2026-07-15T18:54:13.525Z" }, + { url = "https://files.pythonhosted.org/packages/2e/98/59d83c257cd59f0fbaf9d9ddb26b744a576760dfd1ae16e516408894a02b/coverage-7.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688", size = 223973, upload-time = "2026-07-15T18:54:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/2d285c8bef5c4f695d120c1c96dc11715638aa8e134069f210bb6a62a9fe/coverage-7.15.2-cp311-cp311-win_arm64.whl", hash = "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934", size = 223519, upload-time = "2026-07-15T18:54:16.803Z" }, + { url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" }, + { url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" }, + { url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" }, + { url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" }, + { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/2f/ec5241c38e7fa0fe6c26bfc450e78b9489a6c3c08b394b85d2c10e506975/librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5", size = 148654, upload-time = "2026-07-08T12:24:30.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1a/d651e18d3ee7aa2879322368c4f278bb7ecaa6b90caadfdec4ebfa8389f3/librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547", size = 153537, upload-time = "2026-07-08T12:24:31.773Z" }, + { url = "https://files.pythonhosted.org/packages/45/18/10bff2122577246009d9619b6569596daf69b7648812f997ca9ca0426f60/librt-0.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2", size = 494336, upload-time = "2026-07-08T12:24:33.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/87dfee871b852970f137fdeae8e2ca356c5ab38e6f21d2a3299535fc3159/librt-0.13.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929", size = 485393, upload-time = "2026-07-08T12:24:34.324Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d5/625447a8c0441ff5f15f4ac5e1d323fb9d4d256ebfde7a3c8e003f646057/librt-0.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a", size = 515382, upload-time = "2026-07-08T12:24:35.575Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d8/1c8c49ea04235960426444deece9092a6b3a9587a850a81bae2335317411/librt-0.13.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac", size = 509483, upload-time = "2026-07-08T12:24:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/6f/65/f1760fc48050e215201a03506c32b7270159088d01f64557b53e39e74a45/librt-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7", size = 532503, upload-time = "2026-07-08T12:24:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/18/1b/793e281dcf494879eff99f642b63ebc9c7c58694a1c2d1e93362a22c7041/librt-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40", size = 537027, upload-time = "2026-07-08T12:24:39.34Z" }, + { url = "https://files.pythonhosted.org/packages/69/45/0801bbb40c9eea795d3dd3ce91c4c5f3fe7d42d23ec4be3e8cb283bcc754/librt-0.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a", size = 517100, upload-time = "2026-07-08T12:24:40.907Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6c/eb5f514f8e29d4924bc0ff4601dd7b4175557e182e7c0721e84cffa39b8a/librt-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde", size = 558653, upload-time = "2026-07-08T12:24:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/f140100d1b59fe87ff40b5ecbb4e27924335b189a784e230ee465452f6c2/librt-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8", size = 104402, upload-time = "2026-07-08T12:24:43.668Z" }, + { url = "https://files.pythonhosted.org/packages/22/7c/57e40fef7cfb61869341cb28bdcefe8a950bebcbecca74a397bae14dce4a/librt-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc", size = 125002, upload-time = "2026-07-08T12:24:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029, upload-time = "2026-07-08T12:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036, upload-time = "2026-07-08T12:24:47.257Z" }, + { url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062, upload-time = "2026-07-08T12:24:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510, upload-time = "2026-07-08T12:24:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909, upload-time = "2026-07-08T12:24:51.135Z" }, + { url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620, upload-time = "2026-07-08T12:24:52.358Z" }, + { url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363, upload-time = "2026-07-08T12:24:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209, upload-time = "2026-07-08T12:24:55.138Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254, upload-time = "2026-07-08T12:24:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611, upload-time = "2026-07-08T12:24:57.561Z" }, + { url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906, upload-time = "2026-07-08T12:24:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852, upload-time = "2026-07-08T12:25:00.065Z" }, + { url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832, upload-time = "2026-07-08T12:25:01.148Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/09/f2f5f45dae0c9a0891e4751a73312730e009395102e5d72a22a976cca41f/mypy-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc", size = 14927774, upload-time = "2026-07-13T11:28:38.224Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/345367effd3a6877275a94d481614bfca983f45e028c6290e2cc54603811/mypy-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3", size = 14000127, upload-time = "2026-07-13T11:30:19.57Z" }, + { url = "https://files.pythonhosted.org/packages/99/6c/a10b7a7b9f0a755fb94e27ae834d4cea9ad6c5221f9325eef8f182641feb/mypy-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805", size = 14229437, upload-time = "2026-07-13T11:28:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bd/a26a602acb1bbf849fa4bdac4bc657ee2f11c0c2a764a2cc87a5304e865c/mypy-2.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117", size = 15171457, upload-time = "2026-07-13T11:29:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/124f462bef69bcbc90b9358088460b6091954a3e004852fcd9948db617a5/mypy-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5", size = 15478281, upload-time = "2026-07-13T11:32:23.413Z" }, + { url = "https://files.pythonhosted.org/packages/db/a4/8bdca6a8ac8d856d82ed049144af2721245a135c2e8001d3890c93975852/mypy-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494", size = 11148008, upload-time = "2026-07-13T11:34:17.332Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/490eea348e60ba50decec20bc750605444149a5d7a8cc560042f90ba2c75/mypy-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee", size = 10142329, upload-time = "2026-07-13T11:32:52.116Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b9/d75b3082b05f1b3028828aeb18e74ae5ab0a0936051bbf1f32f59f654747/mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", size = 14838725, upload-time = "2026-07-13T11:32:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/50/79a65c6ea6e115bc73296038a4543b2d5c91f07912b918a2c616a2514bba/mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", size = 13911128, upload-time = "2026-07-13T11:32:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/90/48/e11ed7716c26953ca321f726e452e374dbf81a6f2b8b212ec02af29b6b8f/mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", size = 14146742, upload-time = "2026-07-13T11:33:03.313Z" }, + { url = "https://files.pythonhosted.org/packages/06/72/6807565b1c4861ef66f7fdd98b51c61556356eab80235717b46c53bb8627/mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", size = 15081418, upload-time = "2026-07-13T11:31:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/00/80/1ea14c5d80e589e415973db3e47c78c2219a305b808b2b506395342c1d79/mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", size = 15328164, upload-time = "2026-07-13T11:31:35.723Z" }, + { url = "https://files.pythonhosted.org/packages/37/28/8223157404a3d51920078459c37f80fbdc590e1d8ea049dc5ce48643022a/mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", size = 11136472, upload-time = "2026-07-13T11:27:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/ea27e5959c5f258585a756b252031f3b313583d81b5064b2bebc41d3706b/mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", size = 10135800, upload-time = "2026-07-13T11:30:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[[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 = "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 = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-testmon" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/1d/3e4230cc67cd6205bbe03c3527500c0ccaf7f0c78b436537eac71590ee4a/pytest_testmon-2.2.0.tar.gz", hash = "sha256:01f488e955ed0e0049777bee598bf1f647dd524e06f544c31a24e68f8d775a51", size = 23108, upload-time = "2025-12-01T07:30:24.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/55/ebb3c2f59fb089f08d00f764830d35780fc4e4c41dffcadafa3264682b65/pytest_testmon-2.2.0-py3-none-any.whl", hash = "sha256:2604ca44a54d61a2e830d9ce828b41a837075e4ebc1f81b148add8e90d34815b", size = 25199, upload-time = "2025-12-01T07:30:23.623Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, + { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, + { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] From fdd24c61dcf92e5f6280180695e716bf38f1c1ba Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:14:40 +0200 Subject: [PATCH 07/12] style: ruff-format markdown code blocks (align inline comments to PEP 8) --- README.md | 28 ++++++++++++++-------------- docs/FEATURES.md | 46 +++++++++++++++++++++++----------------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 52dd972..27db819 100644 --- a/README.md +++ b/README.md @@ -59,32 +59,32 @@ maturin develop --release import chem_engine as ce # Parse SMILES -mol = ce.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") # aspirin -print(mol.num_atoms) # 13 -print(mol.num_bonds) # 13 -print(mol.amw) # ~180 (heavy atoms only) -print(mol.num_rotatable_bonds) # 3 +mol = ce.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") # aspirin +print(mol.num_atoms) # 13 +print(mol.num_bonds) # 13 +print(mol.amw) # ~180 (heavy atoms only) +print(mol.num_rotatable_bonds) # 3 # Canonical SMILES can = ce.canonicalize(mol) -print(can) # deterministic string +print(can) # deterministic string # 2D layout mol2d = ce.generate_2d_coords(mol) -print(mol2d.coords_2d) # [[x, y], ...] +print(mol2d.coords_2d) # [[x, y], ...] # 3D embedding mol3d = ce.generate_3d_coords(mol) -print(mol3d.coords_3d) # [[x, y, z], ...] +print(mol3d.coords_3d) # [[x, y, z], ...] # Fingerprint and similarity -fp = mol.get_fingerprint() # 2048-bit ECFP2 -sim = mol.similarity(mol) # 1.0 +fp = mol.get_fingerprint() # 2048-bit ECFP2 +sim = mol.similarity(mol) # 1.0 print(sim) # Substructure search benzene = ce.parse_smiles("c1ccccc1") -print(mol.has_substruct_match(benzene)) # True +print(mol.has_substruct_match(benzene)) # True # Tautomers acetone = ce.parse_smiles("CC(=O)C") @@ -106,16 +106,16 @@ from rdkit import Chem # chem-engine -> RDKit rust_mol = ce.parse_smiles("c1ccccc1") rd_mol = to_rdkit(rust_mol) -print(Chem.MolToSmiles(rd_mol)) # RDKit canonical SMILES +print(Chem.MolToSmiles(rd_mol)) # RDKit canonical SMILES # RDKit -> chem-engine rd_mol = Chem.MolFromSmiles("CC(=O)O") rust_mol = from_rdkit(rd_mol) -print(rust_mol.num_atoms) # 4 +print(rust_mol.num_atoms) # 4 # Typical pattern: fast Rust pipeline, RDKit for accuracy-critical steps rust_mol = ce.generate_3d_coords(ce.parse_smiles(smiles)) -rd_mol = to_rdkit(rust_mol) # hand off to RDKit +rd_mol = to_rdkit(rust_mol) # hand off to RDKit ``` --- diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 773d842..0772f9e 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -66,9 +66,9 @@ that is ~5-8x faster than RDKit's parser for typical drug-like molecules. ```python import chem_engine as ce -mol = ce.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") # aspirin -print(mol.num_atoms) # 13 -print(mol.num_bonds) # 13 +mol = ce.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") # aspirin +print(mol.num_atoms) # 13 +print(mol.num_bonds) # 13 ``` --- @@ -152,9 +152,9 @@ mol.coords_3d = [[0.0, 0.0, 0.0], ...] import chem_engine as ce mol = ce.RustMolecule() -mol.add_atom(ce.Atom(6)) # carbon -mol.add_atom(ce.Atom(8)) # oxygen -mol.add_bond(0, 1, ce.BondType.Double) # C=O (formaldehyde) +mol.add_atom(ce.Atom(6)) # carbon +mol.add_atom(ce.Atom(8)) # oxygen +mol.add_bond(0, 1, ce.BondType.Double) # C=O (formaldehyde) print(mol.num_atoms, mol.num_bonds) # 2, 1 ``` @@ -176,7 +176,7 @@ Generates a canonical SMILES string using a Morgan-rank DFS traversal. ```python mol1 = ce.parse_smiles("CCO") mol2 = ce.parse_smiles("OCC") -assert ce.canonicalize(mol1) == ce.canonicalize(mol2) # True +assert ce.canonicalize(mol1) == ce.canonicalize(mol2) # True ``` **Speedup vs RDKit:** ~9x @@ -195,7 +195,7 @@ Assigns 2D (x, y) coordinates to each heavy atom using a **force-directed layout ```python mol = ce.parse_smiles("c1ccccc1") mol = ce.generate_2d_coords(mol) -coords = mol.coords_2d # list of [x, y] for each atom +coords = mol.coords_2d # list of [x, y] for each atom ``` **Speedup vs RDKit `Compute2DCoords`:** ~2.8x @@ -217,7 +217,7 @@ Returns a new `RustMolecule` with `coords_3d` populated. ```python mol = ce.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") # aspirin mol = ce.generate_3d_coords(mol) -coords = mol.coords_3d # list of [x, y, z] for each heavy atom +coords = mol.coords_3d # list of [x, y, z] for each heavy atom ``` **Speedup vs RDKit `EmbedMolecule` (ETKDG):** ~37x @@ -234,7 +234,7 @@ Descriptors are computed as properties on `RustMolecule`. ```python mol = ce.parse_smiles("[H]O[H]") -print(mol.amw) # 18.015 +print(mol.amw) # 18.015 ``` AMW = sum of heavy-atom masses + explicit H masses (1.008 Da per H). @@ -248,7 +248,7 @@ Use `[H]O[H]` form or convert via RDKit for full-precision MW. ```python mol = ce.parse_smiles("CCCC") -print(mol.num_rotatable_bonds) # 1 +print(mol.num_rotatable_bonds) # 1 ``` Definition: a single bond that is: @@ -272,7 +272,7 @@ Produces a **2048-bit ECFP2-style Morgan fingerprint**: - Bits set at every round for every atom ```python -fp = mol.get_fingerprint() # list of 2048 booleans +fp = mol.get_fingerprint() # list of 2048 booleans ``` ### Tanimoto Similarity @@ -282,10 +282,10 @@ fp = mol.get_fingerprint() # list of 2048 booleans Computes the Tanimoto (Jaccard) coefficient over the 2048-bit fingerprints. ```python -ethanol = ce.parse_smiles("CCO") +ethanol = ce.parse_smiles("CCO") ethylamine = ce.parse_smiles("CCN") -print(ethanol.similarity(ethylamine)) # ~0.20 -print(ethanol.similarity(ethanol)) # 1.0 +print(ethanol.similarity(ethylamine)) # ~0.20 +print(ethanol.similarity(ethanol)) # 1.0 ``` Properties: @@ -308,10 +308,10 @@ Checks whether `query` is a subgraph of `target` using backtracking VF-style atom-by-atom assignment with exact bond-type and aromaticity matching. ```python -benzene = ce.parse_smiles("c1ccccc1") -aspirin = ce.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") -print(aspirin.has_substruct_match(benzene)) # True -print(benzene.has_substruct_match(aspirin)) # False +benzene = ce.parse_smiles("c1ccccc1") +aspirin = ce.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") +print(aspirin.has_substruct_match(benzene)) # True +print(benzene.has_substruct_match(aspirin)) # False ``` Matching rules: @@ -337,7 +337,7 @@ Applies 1,3 proton-shift rules to generate tautomers: ```python acetone = ce.parse_smiles("CC(=O)C") tautomers = acetone.enumerate_tautomers() -print(len(tautomers)) # >= 2 (keto + enol) +print(len(tautomers)) # >= 2 (keto + enol) ``` All tautomers have the same atom count as the input. @@ -370,7 +370,7 @@ thread pool (all available CPU cores). ```python smiles_list = ["CCO", "CCCC", "c1ccccc1"] * 10_000 -molecules = ce.batch_parse_smiles(smiles_list) # parallel, uses all cores +molecules = ce.batch_parse_smiles(smiles_list) # parallel, uses all cores ``` Properties: @@ -395,7 +395,7 @@ Uses V2000 MolBlock as the interchange format. from chem_engine.utils import to_rdkit rust_mol = ce.parse_smiles("c1ccccc1") -rd_mol = to_rdkit(rust_mol) # rdkit.Chem.Mol +rd_mol = to_rdkit(rust_mol) # rdkit.Chem.Mol smiles = Chem.MolToSmiles(rd_mol) # RDKit canonical SMILES ``` @@ -405,7 +405,7 @@ smiles = Chem.MolToSmiles(rd_mol) # RDKit canonical SMILES from chem_engine.utils import from_rdkit rd_mol = Chem.MolFromSmiles("CC(=O)Oc1ccccc1C(=O)O") -rust_mol = from_rdkit(rd_mol) # RustMolecule +rust_mol = from_rdkit(rd_mol) # RustMolecule ``` ### What is preserved in round-trip From 134f40e3900639e0d2bb81b8855fe4f84259f5ac Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:17:05 +0200 Subject: [PATCH 08/12] ci: run lint via pre-commit hooks (no drift vs local); add pre-commit dev dep --- .github/workflows/ci.yml | 13 ++++++++----- pyproject.toml | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 704b1c0..201e360 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,12 @@ on: jobs: # ── Job 1: lint (fast — blocks everything else if it fails) ────────────────── + # Runs pre-commit hooks directly so CI uses the exact same ruff version and + # config as local development — no drift between the two. + # SKIP the pytest hooks (testmon/xdist): those need the compiled Rust + # extension and run in the dedicated test job instead. lint: - name: Lint & type-check + name: Lint & format (pre-commit) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -18,12 +22,11 @@ jobs: version: "latest" python-version: "3.11" - - name: Install dev tools + - name: Install dev dependencies (includes pre-commit) run: uv sync - - run: uv run ruff check . - - run: uv run ruff format --check . - - run: uv run mypy chem_engine/ --ignore-missing-imports + - name: Run pre-commit hooks + run: SKIP=pytest-testmon,pytest-xdist-full uv run pre-commit run --all-files # ── Job 2: build Rust extension + run tests ────────────────────────────────── test: diff --git a/pyproject.toml b/pyproject.toml index d78a9b4..771b677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ module-name = "chem_engine._rust" # ── dev dependencies ────────────────────────────────────────────────────────── [dependency-groups] dev = [ + "pre-commit>=3.0", "pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.11", From 8541197284cb1779421ad267ab21dafd6c70878a Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:19:25 +0200 Subject: [PATCH 09/12] fix: add maturin to dev dependencies so uv sync makes it available --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 771b677..34907cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ module-name = "chem_engine._rust" # ── dev dependencies ────────────────────────────────────────────────────────── [dependency-groups] dev = [ + "maturin>=1.5,<2.0", "pre-commit>=3.0", "pytest>=8.0", "pytest-cov>=5.0", From f95313d7b16ae23807523d87458abfe560086be3 Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:23:43 +0200 Subject: [PATCH 10/12] fix: drop coverage threshold (Rust logic unmeasurable by Python cov); drop redundant --ignore (skipif handles it) --- .github/workflows/ci.yml | 4 +--- pyproject.toml | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 201e360..c4845d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: - name: Build Rust extension (in-place) run: uv run maturin develop --release - - name: Run tests in parallel with coverage + - name: Run tests in parallel run: > uv run pytest tests/ -n auto @@ -58,8 +58,6 @@ jobs: -q --cov=chem_engine --cov-report=term-missing - --cov-fail-under=80 - --ignore=tests/test_correctness_vs_rdkit.py # ── Job 3: RDKit cross-validation (optional, runs on main only) ────────────── rdkit-validation: diff --git a/pyproject.toml b/pyproject.toml index 34907cb..7260e7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,12 +34,15 @@ dev = [ testpaths = ["tests"] # ── coverage ────────────────────────────────────────────────────────────────── +# NOTE: chem-engine is a Rust-Python hybrid. The business logic lives in Rust +# (src/) which Python's coverage tool cannot measure. The Python layer +# (chem_engine/utils.py) is a thin interop wrapper ~70 lines. A numeric +# threshold is therefore meaningless — coverage is reported for visibility only. [tool.coverage.run] source = ["chem_engine"] omit = ["tests/*"] [tool.coverage.report] -fail_under = 80 show_missing = true # ── ruff ────────────────────────────────────────────────────────────────────── From cb33be9b9eb14efc872910f2581de292d5b2c112 Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:26:48 +0200 Subject: [PATCH 11/12] docs: replace pip/venv setup with uv; add dev environment section --- README.md | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 27db819..7fcaaf2 100644 --- a/README.md +++ b/README.md @@ -36,19 +36,28 @@ See [docs/BENCHMARKS.md](docs/BENCHMARKS.md) for full throughput tables at 1K, 5 ## Installation -### From source (requires Rust toolchain + maturin) +### From source (requires Rust toolchain) ```bash -# Install Rust: https://rustup.rs +# 1. Install Rust: https://rustup.rs curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -# Install maturin -pip install maturin +# 2. Install uv (if not already installed): https://docs.astral.sh/uv/ +curl -LsSf https://astral.sh/uv/install.sh | sh -# Build and install chem-engine +# 3. Clone and build git clone https://github.com/vandan-revanur/chem-engine.git cd chem-engine -maturin develop --release +uv sync # creates .venv, installs all dev deps (incl. maturin) +uv run maturin develop --release # compiles Rust extension in-place +``` + +### Dev environment setup (first time) + +```bash +uv run detect-secrets scan > .secrets.baseline # initialise secrets baseline +uv run pre-commit install --install-hooks # hook runs on git commit +uv run pre-commit install --hook-type pre-push # hook runs on git push ``` --- @@ -168,11 +177,17 @@ See [docs/BENCHMARKS.md](docs/BENCHMARKS.md) for full benchmark methodology and ## Running the tests ```bash -# Full test suite (481 tests, 0 failures) -python -m pytest tests/ -q +# Full test suite in parallel (286 tests, 0 failures) +uv run pytest tests/ -n auto -q -# With verbose output -python -m pytest tests/ -v +# With coverage report +uv run pytest tests/ -n auto --cov=chem_engine --cov-report=term-missing + +# Affected tests only (fast, uses testmon — re-runs only tests touching changed files) +uv run pytest tests/ --testmon + +# RDKit cross-validation (requires rdkit: uv pip install rdkit) +uv run pytest tests/test_correctness_vs_rdkit.py -v ``` Test files: @@ -251,3 +266,5 @@ See [docs/FEATURES.md#limitations](docs/FEATURES.md#13-limitations-and-known-gap ## License [MIT](LICENSE) - Copyright (c) 2026 Vandan Revanur + + From 3eb53e8339c9e9a01efe03265538f91502b8119e Mon Sep 17 00:00:00 2001 From: vandan revanur Date: Fri, 31 Jul 2026 17:30:10 +0200 Subject: [PATCH 12/12] Fix README --- README.md | 2 - uv.lock | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7fcaaf2..a8741b3 100644 --- a/README.md +++ b/README.md @@ -266,5 +266,3 @@ See [docs/FEATURES.md#limitations](docs/FEATURES.md#13-limitations-and-known-gap ## License [MIT](LICENSE) - Copyright (c) 2026 Vandan Revanur - - diff --git a/uv.lock b/uv.lock index 4232889..91b177e 100644 --- a/uv.lock +++ b/uv.lock @@ -47,13 +47,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "chem-engine" source = { editable = "." } [package.dev-dependencies] dev = [ + { name = "maturin" }, { name = "mypy" }, + { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-testmon" }, @@ -65,7 +76,9 @@ dev = [ [package.metadata.requires-dev] dev = [ + { name = "maturin", specifier = ">=1.5,<2.0" }, { name = "mypy", specifier = ">=1.0" }, + { name = "pre-commit", specifier = ">=3.0" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-cov", specifier = ">=5.0" }, { name = "pytest-testmon", specifier = ">=2.2.0" }, @@ -185,6 +198,15 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[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 = "exceptiongroup" version = "1.3.1" @@ -206,6 +228,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] +[[package]] +name = "filelock" +version = "3.32.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -302,6 +342,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, ] +[[package]] +name = "maturin" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, + { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, + { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, +] + [[package]] name = "mypy" version = "2.3.0" @@ -371,6 +435,15 @@ 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 = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -389,6 +462,15 @@ 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.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -398,6 +480,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -465,6 +563,83 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] +[[package]] +name = "python-discovery" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/51/276f964496a5714ab9f320896195639086881c2b39c03b5ad13de84acbb8/python_discovery-1.5.0.tar.gz", hash = "sha256:3e014c6327154d3dda27939a9a0dc9c5c000439f1906d3f303b48f984bd2ecef", size = 72483, upload-time = "2026-07-21T13:14:14.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/7b/14882602ddee241d7984a742fcb423cb4a30fb0d6efc546ac3129fba475a/python_discovery-1.5.0-py3-none-any.whl", hash = "sha256:70c4fc61b4e7404e44f01d6fc44a715c4d685ca6cea83d295922f05891877c98", size = 34205, upload-time = "2026-07-21T13:14:13.398Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + [[package]] name = "ruff" version = "0.16.1" @@ -552,3 +727,19 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3 wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] + +[[package]] +name = "virtualenv" +version = "21.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/fa/18004e5cb15541ad2a68ff219c755233b012b12d4ec8663d06a258082bec/virtualenv-21.7.1.tar.gz", hash = "sha256:d0dbfaa5483487baea28d7210ef8d24c9d1bd0f10f449eeb215568825a9b334e", size = 5525237, upload-time = "2026-07-30T15:40:36.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a7/ded126c19495158a05c7202b3389139839d4cf78d622d453867778e0f7a8/virtualenv-21.7.1-py3-none-any.whl", hash = "sha256:6394973f990536e34c05157179146c020284c42fe01da1dfeb0ba16c345280d9", size = 5504576, upload-time = "2026-07-30T15:40:34.512Z" }, +]