From 4d48a0a67b2889b03eb65b5ed8136d133b294f12 Mon Sep 17 00:00:00 2001 From: s28ale-maker Date: Sat, 4 Jul 2026 16:33:35 -0400 Subject: [PATCH] feat: add soulcap-lookup, soulcap-tokens, type checking, and CI improvements - soulcap-lookup: new CLI to search CL terms via EBI OLS4 REST API - soulcap-tokens: new CLI to extract distinct marker tokens from the Marker Combinations CSV (M1 prerequisite) - extract_markers() / extract_markers_from_csv() added to marker_syntax - Fix __version__ duplication: read from importlib.metadata at runtime - Add mypy + pandas-stubs to dev deps; mypy clean across all 7 modules - Fix 4 type bugs surfaced by mypy (None narrowing, Hashable index, ExcelFile stubs, mixed params dict) - CI: split into lint + test jobs; add ruff format --check; add uv sync --frozen; add mypy type-check step - Add Dependabot config for Actions and pip deps - Add pre-commit config (ruff check + format) - Create marker_mappings/ directory for tracked mapping artifacts - Resolve merge conflicts from upstream (pyproject.toml, marker_syntax, marker_validation report) Co-Authored-By: Claude Sonnet 4.6 --- .claude/hooks/validate_report_quotes.py | 9 +- .github/dependabot.yml | 11 + .github/workflows/tests.yml | 30 ++- .pre-commit-config.yaml | 7 + marker_mappings/.gitkeep | 0 pyproject.toml | 10 + reports/marker_validation.md | 60 ++--- src/soulcap_cl_mapping/__init__.py | 7 +- src/soulcap_cl_mapping/cl_pro.py | 6 +- src/soulcap_cl_mapping/marker_syntax.py | 113 ++++++++- src/soulcap_cl_mapping/ols4_lookup.py | 172 +++++++++++++ src/soulcap_cl_mapping/snippet_cache.py | 8 +- src/soulcap_cl_mapping/sync_sheets.py | 6 +- tests/test_marker_syntax.py | 138 ++++++++++- tests/test_ols4_lookup.py | 313 ++++++++++++++++++++++++ tests/test_report_validator.py | 58 ++++- tests/test_snippet_cache.py | 105 ++++++-- tests/test_sync_sheets.py | 22 +- uv.lock | 229 ++++++++++++++++- 19 files changed, 1206 insertions(+), 98 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .pre-commit-config.yaml create mode 100644 marker_mappings/.gitkeep create mode 100644 src/soulcap_cl_mapping/ols4_lookup.py create mode 100644 tests/test_ols4_lookup.py diff --git a/.claude/hooks/validate_report_quotes.py b/.claude/hooks/validate_report_quotes.py index de3c276..55fb752 100755 --- a/.claude/hooks/validate_report_quotes.py +++ b/.claude/hooks/validate_report_quotes.py @@ -86,7 +86,9 @@ def main() -> int: try: from soulcap_cl_mapping import report_validator except Exception as exc: # pragma: no cover - defensive - print(f"validate_report_quotes: import failed ({exc}); allowing", file=sys.stderr) + print( + f"validate_report_quotes: import failed ({exc}); allowing", file=sys.stderr + ) return 0 run_id = path.parent.name @@ -104,7 +106,10 @@ def main() -> int: evidence = snippet_cache.load_snippet_texts(run_id, qid, None) except Exception as exc: # pragma: no cover - defensive - print(f"validate_report_quotes: cache read failed ({exc}); allowing", file=sys.stderr) + print( + f"validate_report_quotes: cache read failed ({exc}); allowing", + file=sys.stderr, + ) return 0 if not evidence: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..696b5d7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d1c05a..20ea214 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,6 +6,31 @@ on: branches: [main] jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Set up Python + run: uv python install 3.12 + + - name: Install dependencies + run: uv sync --frozen --extra dev + + - name: Lint + run: uv run ruff check . + + - name: Format check + run: uv run ruff format --check . + + - name: Type check + run: uv run mypy src/soulcap_cl_mapping + test: runs-on: ubuntu-latest strategy: @@ -25,10 +50,7 @@ jobs: run: uv python install ${{ matrix.python-version }} - name: Install dependencies - run: uv sync --extra dev - - - name: Lint - run: uv run ruff check . + run: uv sync --frozen --extra dev - name: Run tests (with coverage gate) run: uv run pytest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ebc1cb4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.19 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/marker_mappings/.gitkeep b/marker_mappings/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 2e2b61b..8ca1533 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,12 +19,16 @@ soulcap-validate = "soulcap_cl_mapping.marker_syntax:main" soulcap-cache = "soulcap_cl_mapping.snippet_cache:main" soulcap-validate-report = "soulcap_cl_mapping.report_validator:main" soulcap-cl-pro = "soulcap_cl_mapping.cl_pro:main" +soulcap-lookup = "soulcap_cl_mapping.ols4_lookup:main" +soulcap-tokens = "soulcap_cl_mapping.marker_syntax:tokens_main" [project.optional-dependencies] dev = [ "pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.6", + "mypy>=1.10", + "pandas-stubs>=2.2", ] [build-system] @@ -41,3 +45,9 @@ addopts = "--cov=soulcap_cl_mapping --cov-report=term-missing --cov-fail-under=8 [tool.coverage.run] branch = true source = ["soulcap_cl_mapping"] + +[tool.mypy] +python_version = "3.12" +disallow_untyped_defs = true +warn_return_any = true +warn_unused_ignores = true diff --git a/reports/marker_validation.md b/reports/marker_validation.md index 8ddad59..77aa1f4 100644 --- a/reports/marker_validation.md +++ b/reports/marker_validation.md @@ -1,39 +1,41 @@ -# Marker Syntax Validation +# Marker Syntax Validation -> **Auto-generated** on each `soulcap-sync` by the EBNF validator (`soulcap_cl_mapping.marker_syntax`). Do not edit by hand — edit the Google Sheet master and re-sync. +> **Auto-generated** on each `soulcap-sync` by the EBNF validator (`soulcap_cl_mapping.marker_syntax`). Do not edit by hand — edit the Google Sheet master and re-sync. - Source: `marker_combinations.csv` -- Cells checked: **256** -- Invalid cells: **24** -- Grammar: [../MARKER_SYNTAX.md](../MARKER_SYNTAX.md) §2 +- Cells checked: **390** +- Invalid cells: **26** +- Grammar: [../MARKER_SYNTAX.md](../MARKER_SYNTAX.md) §2 ## Invalid cells | Sheet row | Subset | Column | Error | Value | |-----------|--------|--------|-------|-------| -| 8 | ILC | Ideal phenotypic markers | invalid marker token "don't" | `Confirm that ILC don't bind to CD14/CD19/CD56/CD11c/CD123` | -| 16 | Basophil | Required phenotypic markers | top-level '\|' must be inside a group | `live/ CD45+ CD33+\|CD123+) (CD193+\|FceR1a+\|HLA-DR-\|CD303-)` | -| 18 | Mono | Ideal phenotypic markers | top-level '\|' must be inside a group | `CD64+/-\|CD33+/-)` | -| 20 | NCMo | Ideal phenotypic markers | top-level '\|' must be inside a group | `CD64+/-\|CD33+/-)` | -| 32 | Naive | Required phenotypic markers | missing space before 'CD27-' | `live/ CD45+ (CD20+\|CD19+)CD27- (CD38lo/-\|CD10-) CD24- IgD+` | -| 46 | iNKT | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) (TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+) TCR VB11+` | -| 47 | CD4 Con T | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+) TCRVB11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+` | -| 48 | CD8 ConT | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+) TCRVB11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD8+` | -| 49 | MAIT | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) (TCRVa7.2+\|MR1 Tetramer+) CD161+` | -| 50 | CD4/CD8 ConT memory | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) (TCRVa24-Ja18\|TCRva24\|CD1d-a-GalCer Tetramer) TCRVB11 (TCRVa7.2\|MR1 Tetramer) CD161 CD4 CD8 CD45RA CD197` | -| 51 | CD4/CD8 ConT helper | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) (TCRVa24-Ja18\|TCRva24\|CD1d-a-GalCer Tetramer) TCRVB11 (TCRVa7.2\|MR1 Tetramer) CD161 CD4 CD8 (CD183\|CXCR3) (CD185\|CXCR5) (CD194\|CCR4) (CD196\|CCR6)` | -| 52 | ConT Treg-like | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+) TCRVB11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ CD25+ CD127lo/-` | -| 53 | ConT Tfh-like | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+)+ TCRVB11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185+` | -| 54 | Th1-like | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+)+ TCRVB11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185- CD183+ CD196- CD194-` | -| 55 | Th2-like | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+)+ TCRVB11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185- CD183- CD196- CD194+ CD294+` | -| 56 | Th9-like | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+)+ TCRVB11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185- CD183- CD196- CD194+` | -| 57 | Th17-like | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+)+ TCRVB11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185- CD183- CD196+ CD194+/-` | -| 72 | Vd2 | Required phenotypic markers | invalid marker token '2+' | `CD45+ CD3+ (TCRab-\|TCRgd+) TCR V delta 2+ TCR V gamma 9-` | -| 73 | Vd3 | Required phenotypic markers | invalid marker token '1-' | `CD45+ CD3+ (TCRab-\|TCRgd+) TCR V delta 1- TCR V delta 2-` | -| 73 | Vd3 | Ideal phenotypic markers | invalid marker token '9+/-' | `TCR V gamma 9+/-` | -| 74 | Vd1 | Required phenotypic markers | invalid marker token '1+' | `CD45+ CD3+ (TCRab-\|TCRgd+) TCR V delta 1+` | -| 74 | Vd1 | Ideal phenotypic markers | invalid marker token '2-' | `TCR V delta 2- TCR V gamma 9+/-` | -| 75 | Vg9 | Required phenotypic markers | invalid marker token '9+' | `CD45+ CD3+ (TCRab-\|TCRgd+) TCR V gamma 9+` | -| 75 | Vg9 | Ideal phenotypic markers | invalid marker token '1-' | `TCR V delta 1- TCR V delta 2-` | +| 18 | | Required exclusion | unexpected ')' in group | `CD56- CD19- ([CD123hi]- CD303-) ([CD123hi\|CD193-)` | +| 19 | | Required exclusion | unexpected ')' in group | `CD56- CD19- ([CD123hi]- CD303-) ([CD123hi\|CD193-)` | +| 20 | | Required exclusion | unexpected ')' in group | `CD56- CD19- ([CD123hi]- CD303-) ([CD123hi\|CD193-)` | +| 21 | | Required exclusion | unexpected ')' in group | `CD56- CD19- ([CD123hi]- CD303-) ([CD123hi\|CD193-)` | +| 60 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) (TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCR Vb11+` | +| 75 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) (TCRVa7.2+\|MR1 Tetramer+) CD161+` | +| 76 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- [(TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCR Vb11+]-` | +| 77 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+) TCR VB11+]- CD4+ CD8-` | +| 78 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- [(TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCR Vb11+]- CD4- CD8+` | +| 79 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- [(TCRVa24-Ja18+\|TCRva24+\|CD1d-a-GalCer Tetramer+) TCR VB11+]- CD4+ CD8+` | +| 80 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- [(TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCR Vb11+]- CD4- CD8-` | +| 108 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) (TCRVa24-Ja18\|TCRVa24\|CD1d-a-GalCer Tetramer) TCRVb11 (TCRVa7.2\|MR1 Tetramer) CD161 CD4 CD8 (CD183\|CXCR3) (CD185\|CXCR5) (CD194\|CCR4) (CD196\|CCR6)` | +| 110 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCRVb11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ CD25+ CD127lo/-` | +| 111 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCRVb11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185+` | +| 112 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCRVb11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185- CD183+ CD196- CD194-` | +| 113 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCRVb11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185- CD183- CD196- CD194+ CD294+` | +| 114 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCRVb11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185- CD183- CD196- CD194+` | +| 115 | | Required phenotypic markers | mixed '\|' and space in a group | `CD45+ CD3+ (TCRab+\|TCRgd-) [(TCRVa24-Ja18+\|TCRVa24+\|CD1d-a-GalCer Tetramer+) TCRVb11+]- [(TCRVa7.2+\|MR1 Tetramer+) CD161+]- CD4+ [CD25+ CD127lo/-]- CD185- CD183- CD196+ CD194+/-` | +| 124 | | Required phenotypic markers | invalid marker token '2+' | `CD45+ CD3+ (TCRab-\|TCRgd+) TCR V delta 2+ TCR V gamma 9- TCR Vd1-` | +| 125 | | Required phenotypic markers | invalid marker token '1-' | `CD45+ CD3+ (TCRab-\|TCRgd+) TCR V delta 1- TCR V delta 2- TCR V gamma 9-` | +| 125 | | Ideal phenotypic markers | invalid marker token '9+/-' | `TCR V gamma 9+/-` | +| 126 | | Required phenotypic markers | invalid marker token '1+' | `CD45+ CD3+ (TCRab-\|TCRgd+) TCR V delta 1+ TCR V delta 2- TCR V gamma 9+` | +| 127 | | Required phenotypic markers | invalid marker token '1+' | `CD45+ CD3+ (TCRab-\|TCRgd+) TCR V delta 1+ TCR V delta 2- TCR V gamma 9+/-` | +| 127 | | Ideal phenotypic markers | invalid marker token '2-' | `TCR V delta 2- TCR V gamma 9+/-` | +| 128 | | Required phenotypic markers | invalid marker token '1-' | `CD45+ CD3+ (TCRab-\|TCRgd+) TCR V delta 1- TCR V delta 2- TCR V gamma 9+` | +| 128 | | Ideal phenotypic markers | invalid marker token '1-' | `TCR V delta 1- TCR V delta 2-` | See [marker_string_issues.md](marker_string_issues.md) for the broader (non-syntactic) data-quality review. diff --git a/src/soulcap_cl_mapping/__init__.py b/src/soulcap_cl_mapping/__init__.py index 79d2b30..a9b479e 100644 --- a/src/soulcap_cl_mapping/__init__.py +++ b/src/soulcap_cl_mapping/__init__.py @@ -1,3 +1,8 @@ """SOULCAP ↔ Cell Ontology mapping utilities.""" -__version__ = "0.1.0" +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("soulcap-cl-mapping") +except PackageNotFoundError: + __version__ = "unknown" diff --git a/src/soulcap_cl_mapping/cl_pro.py b/src/soulcap_cl_mapping/cl_pro.py index 8a87284..7d96609 100644 --- a/src/soulcap_cl_mapping/cl_pro.py +++ b/src/soulcap_cl_mapping/cl_pro.py @@ -34,7 +34,7 @@ from collections import defaultdict from datetime import date from pathlib import Path -from typing import Callable +from typing import Any, Callable import certifi from SPARQLWrapper import JSON, SPARQLWrapper @@ -108,7 +108,7 @@ def run_sparql( wrapper.setTimeout(timeout) wrapper.setMethod("POST") wrapper.setQuery(query) - result = wrapper.query().convert() + result: dict[str, Any] = wrapper.query().convert() # type: ignore[assignment] rows: list[dict[str, str]] = [] for binding in result["results"]["bindings"]: rows.append({var: cell["value"] for var, cell in binding.items()}) @@ -392,7 +392,7 @@ def fetch_pr_metadata(query_fn: QueryFn = run_sparql) -> dict[str, dict]: meta: dict[str, dict] = {} for row in query_fn(PR_METADATA_QUERY): xrefs = _split(row.get("xrefs", "")) - entry = { + entry: dict[str, Any] = { "label": (row.get("label", "") or "").split("|")[0], "taxa": [TAXA[t] for t in _split(row.get("taxon", "")) if t in TAXA], "exact": _split(row.get("exact_syn", "")), diff --git a/src/soulcap_cl_mapping/marker_syntax.py b/src/soulcap_cl_mapping/marker_syntax.py index f904874..8d4ec01 100644 --- a/src/soulcap_cl_mapping/marker_syntax.py +++ b/src/soulcap_cl_mapping/marker_syntax.py @@ -147,6 +147,7 @@ def _parse_term(self) -> None: self._parse_group() elif kind == "WORD": self._advance() + assert text is not None _split_word(text) # validates marker + qualifier else: raise MarkerSyntaxError(f"expected marker or group, found {text!r}") @@ -162,7 +163,7 @@ def _parse_group(self) -> None: # optional group-level qualifier: a following WORD that is wholly a # qualifier, e.g. [HLA-DR+ CD11chi]- kind, text = self._peek() - if kind == "WORD" and _is_qualifier(text): + if kind == "WORD" and text is not None and _is_qualifier(text): self._advance() def _parse_inner(self, close: str) -> None: @@ -217,7 +218,7 @@ def _scan(path: str | Path) -> tuple[list[dict], int]: cols = [c for c in MARKER_COLUMNS if c in df.columns] failures: list[dict] = [] n_cells = 0 - for idx, row in df.iterrows(): + for row_num, (_, row) in enumerate(df.iterrows()): for col in cols: value = row[col] if pd.isna(value): @@ -227,7 +228,7 @@ def _scan(path: str | Path) -> tuple[list[dict], int]: if error is not None: failures.append( { - "row": idx + 2, # +1 header, +1 for 1-based + "row": row_num + 2, # +1 header, +1 for 1-based "subset": str(row.get("Subset name", "")).strip(), "column": col, "value": str(value).strip(), @@ -295,7 +296,9 @@ def write_report( """Write/overwrite the Markdown validation report; return its path.""" report_path = Path(report_path) report_path.parent.mkdir(parents=True, exist_ok=True) - report_path.write_text(render_report(failures, source_name, n_cells)) + report_path.write_text( + render_report(failures, source_name, n_cells), encoding="utf-8" + ) return report_path @@ -324,6 +327,108 @@ def validate_marker_csv( return failures +# --------------------------------------------------------------------------- # +# Token extraction +# --------------------------------------------------------------------------- # +def extract_markers(expr: str) -> set[str]: + """Return the distinct marker names in a marker expression. + + Strips qualifiers (e.g. ``CD4+`` → ``CD4``) and grouping symbols. + Skips the live-gate prefix and group-level qualifiers. Returns an empty + set for empty or invalid expressions — never raises. + """ + try: + tokens = _tokenize(expr) + except Exception: # noqa: BLE001 + return set() + markers: set[str] = set() + for kind, text in tokens: + if kind != "WORD": + continue + if text == GATE or _is_qualifier(text): + continue + try: + marker, _ = _split_word(text) + markers.add(marker) + except MarkerSyntaxError: + continue + return markers + + +def extract_markers_from_csv(path: str | Path) -> list[dict]: + """Extract all distinct marker tokens from a Marker Combinations CSV. + + Returns one record per distinct marker token, sorted alphabetically: + ``{"marker_token", "source_columns", "cell_types"}`` where the latter + two are ``|``-separated strings listing which marker columns and which + subset names contain that token. + """ + df = pd.read_csv(path) + cols = [c for c in MARKER_COLUMNS if c in df.columns] + has_subset = "Subset name" in df.columns + + token_info: dict[str, dict[str, set]] = {} + for _, row in df.iterrows(): + subset = str(row["Subset name"]).strip() if has_subset else "" + for col in cols: + value = row[col] + if pd.isna(value): + continue + for marker in extract_markers(str(value)): + if marker not in token_info: + token_info[marker] = {"columns": set(), "cell_types": set()} + token_info[marker]["columns"].add(col) + if subset: + token_info[marker]["cell_types"].add(subset) + + return [ + { + "marker_token": token, + "source_columns": "|".join(sorted(info["columns"])), + "cell_types": "|".join(sorted(info["cell_types"])), + } + for token, info in sorted(token_info.items()) + ] + + +def tokens_main(argv: list[str] | None = None) -> int: + """CLI: extract distinct marker tokens from the Marker Combinations CSV.""" + import argparse + + repo_root = Path(__file__).resolve().parents[2] + default_csv = repo_root / "data" / "marker_combinations.csv" + default_out = repo_root / "marker_mappings" / "marker_tokens.csv" + + parser = argparse.ArgumentParser( + prog="soulcap-tokens", + description="Extract distinct marker tokens from the Marker Combinations CSV.", + ) + parser.add_argument( + "csv", + nargs="?", + type=Path, + default=default_csv, + help="Path to marker_combinations.csv (default: data/).", + ) + parser.add_argument( + "--out", + type=Path, + default=default_out, + help="Output CSV (default: marker_mappings/marker_tokens.csv).", + ) + args = parser.parse_args(argv) + + if not args.csv.exists(): + print(f"error: {args.csv} not found — run `soulcap-sync` first.") + return 2 + + records = extract_markers_from_csv(args.csv) + args.out.parent.mkdir(parents=True, exist_ok=True) + pd.DataFrame(records).to_csv(args.out, index=False) + print(f"Extracted {len(records)} distinct marker token(s) -> {args.out}") + return 0 + + def main(argv: list[str] | None = None) -> int: import argparse diff --git a/src/soulcap_cl_mapping/ols4_lookup.py b/src/soulcap_cl_mapping/ols4_lookup.py new file mode 100644 index 0000000..09b58a4 --- /dev/null +++ b/src/soulcap_cl_mapping/ols4_lookup.py @@ -0,0 +1,172 @@ +"""Look up Cell Ontology terms via the EBI OLS4 REST API. + +Usage: + soulcap-lookup "natural killer cell" # search by name + soulcap-lookup --id CL:0000623 # fetch by CL identifier + soulcap-lookup --rows 10 "T cell" # more results + soulcap-lookup --ontology cl "B cell" # explicit ontology (default: cl) +""" + +from __future__ import annotations + +import argparse +import sys + +import requests + +OLS4_BASE = "https://www.ebi.ac.uk/ols4/api" +OBO_PREFIX = "http://purl.obolibrary.org/obo/" +DEFAULT_ONTOLOGY = "cl" +DEFAULT_ROWS = 5 + + +# --------------------------------------------------------------------------- # +# OLS4 API wrappers +# --------------------------------------------------------------------------- # +def _id_to_iri(obo_id: str) -> str: + """Convert ``CL:0000623`` → ``http://purl.obolibrary.org/obo/CL_0000623``.""" + return OBO_PREFIX + obo_id.replace(":", "_") + + +def search( + query: str, + ontology: str = DEFAULT_ONTOLOGY, + rows: int = DEFAULT_ROWS, + timeout: int = 15, +) -> list[dict]: + """Search OLS4 for terms matching *query* in *ontology*. + + Returns a list of normalised term dicts with keys: + ``obo_id``, ``label``, ``description``, ``synonyms``. + """ + params: dict[str, str | int] = { + "q": query, + "ontology": ontology, + "rows": rows, + "exact": "false", + } + resp = requests.get(f"{OLS4_BASE}/search", params=params, timeout=timeout) + resp.raise_for_status() + docs = resp.json().get("response", {}).get("docs", []) + return [_normalise_doc(d) for d in docs] + + +def fetch_term( + obo_id: str, + ontology: str = DEFAULT_ONTOLOGY, + timeout: int = 15, +) -> dict | None: + """Fetch a single term by its OBO identifier (e.g. ``CL:0000623``). + + Returns a normalised term dict, or ``None`` if not found. + """ + resp = requests.get( + f"{OLS4_BASE}/ontologies/{ontology}/terms", + params={"iri": _id_to_iri(obo_id)}, + timeout=timeout, + ) + resp.raise_for_status() + terms = resp.json().get("_embedded", {}).get("terms", []) + return _normalise_term(terms[0]) if terms else None + + +# --------------------------------------------------------------------------- # +# Normalisation (search and term APIs use slightly different field names) +# --------------------------------------------------------------------------- # +def _normalise_doc(doc: dict) -> dict: + syns = doc.get("synonym") or [] + desc = doc.get("description") or [] + return { + "obo_id": doc.get("obo_id", ""), + "label": doc.get("label", ""), + "description": [desc] if isinstance(desc, str) else list(desc), + "synonyms": [syns] if isinstance(syns, str) else list(syns), + } + + +def _normalise_term(term: dict) -> dict: + desc = term.get("description") or [] + syns = term.get("synonyms") or [] + return { + "obo_id": term.get("obo_id", ""), + "label": term.get("label", ""), + "description": [desc] if isinstance(desc, str) else list(desc), + "synonyms": [syns] if isinstance(syns, str) else list(syns), + } + + +# --------------------------------------------------------------------------- # +# Formatting +# --------------------------------------------------------------------------- # +def format_term(term: dict) -> str: + """Format a single term for terminal display.""" + obo_id = term.get("obo_id", "") + label = term.get("label", "") + desc = term.get("description") or [] + syns = term.get("synonyms") or [] + + indent = " " * (len(obo_id) + 2) + lines = [f"{obo_id} {label}"] + if desc: + text = desc[0] + if len(text) > 200: + text = text[:200] + "..." + lines.append(f"{indent}{text}") + if syns: + lines.append(f"{indent}Synonyms: {', '.join(syns[:5])}") + return "\n".join(lines) + + +# --------------------------------------------------------------------------- # +# CLI +# --------------------------------------------------------------------------- # +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="soulcap-lookup", + description="Look up Cell Ontology terms via EBI OLS4.", + ) + parser.add_argument( + "query", nargs="?", help="Search term (e.g. 'natural killer cell')." + ) + parser.add_argument( + "--id", dest="obo_id", metavar="ID", help="Fetch by OBO id (e.g. CL:0000623)." + ) + parser.add_argument( + "--ontology", default=DEFAULT_ONTOLOGY, help="Ontology to search (default: cl)." + ) + parser.add_argument( + "--rows", + type=int, + default=DEFAULT_ROWS, + help="Max search results (default: 5).", + ) + args = parser.parse_args(argv) + + if not args.obo_id and not args.query: + parser.print_help() + return 2 + + try: + if args.obo_id: + term = fetch_term(args.obo_id, ontology=args.ontology) + if term is None: + print(f"No term found for '{args.obo_id}'.", file=sys.stderr) + return 1 + print(format_term(term)) + else: + results = search(args.query, ontology=args.ontology, rows=args.rows) + if not results: + print(f"No results for '{args.query}'.", file=sys.stderr) + return 1 + print(f"Found {len(results)} result(s) for '{args.query}':\n") + for term in results: + print(format_term(term)) + print() + except Exception as exc: # noqa: BLE001 + print(f"error: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/soulcap_cl_mapping/snippet_cache.py b/src/soulcap_cl_mapping/snippet_cache.py index 1b9b42f..f99c065 100644 --- a/src/soulcap_cl_mapping/snippet_cache.py +++ b/src/soulcap_cl_mapping/snippet_cache.py @@ -243,9 +243,7 @@ def _extract_data(asta_result: dict) -> list: return data if isinstance(data, list) else [] -def load_records( - run_id: str, qid: str, root: str | Path | None = None -) -> list[dict]: +def load_records(run_id: str, qid: str, root: str | Path | None = None) -> list[dict]: """Return all cached snippet records for a question (empty if none).""" path = question_file(run_id, qid, root) if not path.exists(): @@ -351,7 +349,9 @@ def main(argv: list[str] | None = None) -> int: root=args.root, ) total = len(load_records(args.run, args.qid, root=args.root)) - print(f"appended {n} new snippet(s) to {args.qid} (round {args.round}); {total} total") + print( + f"appended {n} new snippet(s) to {args.qid} (round {args.round}); {total} total" + ) return 0 # show diff --git a/src/soulcap_cl_mapping/sync_sheets.py b/src/soulcap_cl_mapping/sync_sheets.py index dfd0086..36ccde5 100644 --- a/src/soulcap_cl_mapping/sync_sheets.py +++ b/src/soulcap_cl_mapping/sync_sheets.py @@ -66,17 +66,17 @@ def explode_to_csv(xlsx_path: Path, data_dir: Path) -> list[Path]: xl = pd.ExcelFile(xlsx_path, engine="openpyxl") written: list[Path] = [] for sheet in xl.sheet_names: - raw = xl.parse(sheet, header=None) + raw = xl.parse(sheet, header=None) # type: ignore[attr-defined] if raw.empty: print(f" skip (empty): {sheet}") continue if sheet in HEADER_ROW_SHEETS: # Keep every column — including currently-empty target columns # such as 'OLS CL identifier' that this project will populate. - df = xl.parse(sheet, header=0).dropna(how="all") + df = xl.parse(sheet, header=0).dropna(how="all") # type: ignore[attr-defined] else: df = raw.dropna(how="all").dropna(axis=1, how="all") - out = data_dir / f"{_slug(sheet)}.csv" + out = data_dir / f"{_slug(str(sheet))}.csv" df.to_csv(out, index=False) written.append(out) print(f" {sheet:40s} -> {out.name} ({df.shape[0]}x{df.shape[1]})") diff --git a/tests/test_marker_syntax.py b/tests/test_marker_syntax.py index ab2426c..831295c 100644 --- a/tests/test_marker_syntax.py +++ b/tests/test_marker_syntax.py @@ -197,9 +197,7 @@ def test_main_returns_1_on_failures(tmp_path, monkeypatch): _write_csv(csv) # keep the report write inside tmp_path monkeypatch.chdir(tmp_path) - monkeypatch.setattr( - ms, "validate_marker_csv", lambda p: [{"x": 1}] - ) + monkeypatch.setattr(ms, "validate_marker_csv", lambda p: [{"x": 1}]) assert ms.main([str(csv)]) == 1 @@ -208,3 +206,137 @@ def test_main_returns_0_when_clean(tmp_path, monkeypatch): _write_csv(csv) monkeypatch.setattr(ms, "validate_marker_csv", lambda p: []) assert ms.main([str(csv)]) == 0 + + +# --------------------------------------------------------------------------- # +# extract_markers +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize( + "expr,expected", + [ + ("CD4+", {"CD4"}), + ("CD4+ CD8-", {"CD4", "CD8"}), + ("(CD4+|CD8-)", {"CD4", "CD8"}), + ("[HLA-DR+ CD11chi]-", {"HLA-DR", "CD11c"}), # group-level qual skipped + ("live/ CD4+", {"CD4"}), # gate prefix skipped + ("live/ CD45+ CD56+/hi CD127-", {"CD45", "CD56", "CD127"}), + ("[(TCRVa7.2+|MR1Tetramer+) CD161+]-", {"TCRVa7.2", "MR1Tetramer", "CD161"}), + ("CD16-/lo", {"CD16"}), + ], +) +def test_extract_markers(expr, expected): + assert ms.extract_markers(expr) == expected + + +def test_extract_markers_empty_string(): + assert ms.extract_markers("") == set() + + +def test_extract_markers_invalid_does_not_raise(): + # Invalid expressions return empty set rather than raising. + assert ms.extract_markers("!!!") == set() + + +def test_extract_markers_deduplicates(): + # Same marker in multiple groups → appears once. + assert ms.extract_markers("(CD3+|CD3-)") == {"CD3"} + + +# --------------------------------------------------------------------------- # +# extract_markers_from_csv +# --------------------------------------------------------------------------- # +def _write_markers_csv(path): + pd.DataFrame( + { + "Subset name": ["NK cell", "T cell", "B cell"], + "Required phenotypic markers": ["CD45+ CD56+", "CD45+ CD3+", "CD45+ CD19+"], + "Ideal exclusion": ["CD3-", None, "CD3-"], + "Required exclusion": [None, None, None], + "Ideal phenotypic markers": [None, "CD4+", None], + } + ).to_csv(path, index=False) + + +def test_extract_markers_from_csv_tokens(tmp_path): + csv = tmp_path / "mc.csv" + _write_markers_csv(csv) + records = ms.extract_markers_from_csv(csv) + tokens = {r["marker_token"] for r in records} + assert tokens == {"CD3", "CD4", "CD19", "CD45", "CD56"} + + +def test_extract_markers_from_csv_sorted(tmp_path): + csv = tmp_path / "mc.csv" + _write_markers_csv(csv) + records = ms.extract_markers_from_csv(csv) + tokens = [r["marker_token"] for r in records] + assert tokens == sorted(tokens) + + +def test_extract_markers_from_csv_provenance(tmp_path): + csv = tmp_path / "mc.csv" + _write_markers_csv(csv) + records = ms.extract_markers_from_csv(csv) + by_token = {r["marker_token"]: r for r in records} + + # CD45 appears in Required phenotypic markers for all 3 subsets + cd45 = by_token["CD45"] + assert "Required phenotypic markers" in cd45["source_columns"] + assert "NK cell" in cd45["cell_types"] + assert "T cell" in cd45["cell_types"] + assert "B cell" in cd45["cell_types"] + + # CD4 only in Ideal phenotypic markers, only for T cell + cd4 = by_token["CD4"] + assert cd4["source_columns"] == "Ideal phenotypic markers" + assert cd4["cell_types"] == "T cell" + + # CD3 spans two columns + cd3 = by_token["CD3"] + cols = set(cd3["source_columns"].split("|")) + assert "Ideal exclusion" in cols + assert "Required phenotypic markers" in cols + + +def test_extract_markers_from_csv_skips_nulls(tmp_path): + csv = tmp_path / "mc.csv" + pd.DataFrame( + { + "Subset name": ["X"], + "Required phenotypic markers": [None], + "Ideal exclusion": [None], + } + ).to_csv(csv, index=False) + assert ms.extract_markers_from_csv(csv) == [] + + +# --------------------------------------------------------------------------- # +# tokens_main CLI +# --------------------------------------------------------------------------- # +def test_tokens_main_writes_csv(tmp_path, capsys): + csv = tmp_path / "mc.csv" + _write_markers_csv(csv) + out = tmp_path / "out" / "tokens.csv" + + rc = ms.tokens_main([str(csv), "--out", str(out)]) + + assert rc == 0 + assert out.exists() + df = pd.read_csv(out) + assert set(df["marker_token"]) == {"CD3", "CD4", "CD19", "CD45", "CD56"} + assert "Extracted 5" in capsys.readouterr().out + + +def test_tokens_main_missing_csv(tmp_path, capsys): + rc = ms.tokens_main([str(tmp_path / "nope.csv")]) + assert rc == 2 + assert "not found" in capsys.readouterr().out + + +def test_tokens_main_creates_output_dir(tmp_path): + csv = tmp_path / "mc.csv" + _write_markers_csv(csv) + out = tmp_path / "deep" / "nested" / "tokens.csv" + rc = ms.tokens_main([str(csv), "--out", str(out)]) + assert rc == 0 + assert out.exists() diff --git a/tests/test_ols4_lookup.py b/tests/test_ols4_lookup.py new file mode 100644 index 0000000..f7fcc3b --- /dev/null +++ b/tests/test_ols4_lookup.py @@ -0,0 +1,313 @@ +"""Tests for soulcap_cl_mapping.ols4_lookup. + +Network is always mocked — these tests never hit EBI OLS4. +""" + +from __future__ import annotations + +from soulcap_cl_mapping import ols4_lookup as ol + + +# --------------------------------------------------------------------------- # +# Helpers +# --------------------------------------------------------------------------- # +class _FakeResp: + def __init__(self, data: dict): + self._data = data + + def raise_for_status(self) -> None: + pass + + def json(self) -> dict: + return self._data + + +def _search_payload(*docs: dict) -> dict: + return {"response": {"numFound": len(docs), "docs": list(docs)}} + + +def _term_payload(*terms: dict) -> dict: + return {"_embedded": {"terms": list(terms)}} + + +def _doc(obo_id: str = "CL:0000623", label: str = "natural killer cell") -> dict: + return { + "obo_id": obo_id, + "label": label, + "description": ["A lymphocyte that can kill target cells."], + "synonym": ["NK cell", "killer cell"], + } + + +def _term(obo_id: str = "CL:0000623", label: str = "natural killer cell") -> dict: + return { + "obo_id": obo_id, + "label": label, + "description": ["A lymphocyte that can kill target cells."], + "synonyms": ["NK cell", "killer cell"], + } + + +# --------------------------------------------------------------------------- # +# Pure functions +# --------------------------------------------------------------------------- # +def test_id_to_iri(): + assert ol._id_to_iri("CL:0000623") == "http://purl.obolibrary.org/obo/CL_0000623" + + +def test_id_to_iri_uberon(): + assert ( + ol._id_to_iri("UBERON:0000955") + == "http://purl.obolibrary.org/obo/UBERON_0000955" + ) + + +def test_normalise_doc_list_fields(): + doc = {"obo_id": "CL:1", "label": "foo", "description": ["d1"], "synonym": ["s1"]} + result = ol._normalise_doc(doc) + assert result["description"] == ["d1"] + assert result["synonyms"] == ["s1"] + + +def test_normalise_doc_string_fields(): + doc = { + "obo_id": "CL:1", + "label": "foo", + "description": "a desc", + "synonym": "a syn", + } + result = ol._normalise_doc(doc) + assert result["description"] == ["a desc"] + assert result["synonyms"] == ["a syn"] + + +def test_normalise_doc_missing_fields(): + result = ol._normalise_doc({}) + assert result == {"obo_id": "", "label": "", "description": [], "synonyms": []} + + +def test_normalise_term(): + term = {"obo_id": "CL:1", "label": "foo", "description": ["d"], "synonyms": ["s"]} + result = ol._normalise_term(term) + assert result["synonyms"] == ["s"] + assert result["description"] == ["d"] + + +def test_normalise_term_missing_fields(): + result = ol._normalise_term({}) + assert result == {"obo_id": "", "label": "", "description": [], "synonyms": []} + + +# --------------------------------------------------------------------------- # +# format_term +# --------------------------------------------------------------------------- # +def test_format_term_full(): + term = { + "obo_id": "CL:0000623", + "label": "natural killer cell", + "description": ["A lymphocyte."], + "synonyms": ["NK cell"], + } + out = ol.format_term(term) + assert "CL:0000623 natural killer cell" in out + assert "A lymphocyte." in out + assert "NK cell" in out + + +def test_format_term_no_desc_no_syns(): + out = ol.format_term( + {"obo_id": "CL:1", "label": "foo", "description": [], "synonyms": []} + ) + assert "CL:1 foo" in out + assert "Synonyms" not in out + + +def test_format_term_truncates_long_description(): + term = { + "obo_id": "CL:1", + "label": "foo", + "description": ["x" * 300], + "synonyms": [], + } + out = ol.format_term(term) + assert "..." in out + # Should not exceed ~210 chars for the description portion + lines = out.splitlines() + assert len(lines[1].strip()) <= 210 + + +def test_format_term_limits_synonyms(): + term = { + "obo_id": "CL:1", + "label": "foo", + "description": [], + "synonyms": [f"syn{i}" for i in range(10)], + } + out = ol.format_term(term) + # Only first 5 synonyms shown + assert "syn4" in out + assert "syn5" not in out + + +# --------------------------------------------------------------------------- # +# search() +# --------------------------------------------------------------------------- # +def test_search_returns_normalised_results(monkeypatch): + captured = {} + + def fake_get(url, params, timeout): + captured["params"] = params + return _FakeResp(_search_payload(_doc())) + + monkeypatch.setattr(ol.requests, "get", fake_get) + results = ol.search("NK cell", ontology="cl", rows=3) + + assert len(results) == 1 + assert results[0]["obo_id"] == "CL:0000623" + assert results[0]["label"] == "natural killer cell" + assert captured["params"]["q"] == "NK cell" + assert captured["params"]["ontology"] == "cl" + assert captured["params"]["rows"] == 3 + + +def test_search_empty_response(monkeypatch): + monkeypatch.setattr( + ol.requests, "get", lambda *a, **k: _FakeResp({"response": {"docs": []}}) + ) + assert ol.search("nothing") == [] + + +def test_search_missing_response_key(monkeypatch): + monkeypatch.setattr(ol.requests, "get", lambda *a, **k: _FakeResp({})) + assert ol.search("x") == [] + + +# --------------------------------------------------------------------------- # +# fetch_term() +# --------------------------------------------------------------------------- # +def test_fetch_term_returns_normalised(monkeypatch): + captured = {} + + def fake_get(url, params, timeout): + captured["iri"] = params.get("iri") + return _FakeResp(_term_payload(_term())) + + monkeypatch.setattr(ol.requests, "get", fake_get) + result = ol.fetch_term("CL:0000623") + + assert result is not None + assert result["obo_id"] == "CL:0000623" + assert result["label"] == "natural killer cell" + assert captured["iri"] == "http://purl.obolibrary.org/obo/CL_0000623" + + +def test_fetch_term_returns_none_when_missing(monkeypatch): + monkeypatch.setattr( + ol.requests, "get", lambda *a, **k: _FakeResp({"_embedded": {"terms": []}}) + ) + assert ol.fetch_term("CL:9999999") is None + + +def test_fetch_term_returns_none_on_missing_embedded(monkeypatch): + monkeypatch.setattr(ol.requests, "get", lambda *a, **k: _FakeResp({})) + assert ol.fetch_term("CL:1") is None + + +# --------------------------------------------------------------------------- # +# main() / CLI +# --------------------------------------------------------------------------- # +def test_main_search_mode(monkeypatch, capsys): + monkeypatch.setattr( + ol.requests, "get", lambda *a, **k: _FakeResp(_search_payload(_doc())) + ) + rc = ol.main(["natural killer cell"]) + assert rc == 0 + out = capsys.readouterr().out + assert "CL:0000623" in out + assert "natural killer cell" in out + assert "Found 1 result" in out + + +def test_main_id_mode(monkeypatch, capsys): + monkeypatch.setattr( + ol.requests, "get", lambda *a, **k: _FakeResp(_term_payload(_term())) + ) + rc = ol.main(["--id", "CL:0000623"]) + assert rc == 0 + assert "CL:0000623" in capsys.readouterr().out + + +def test_main_id_not_found(monkeypatch, capsys): + monkeypatch.setattr( + ol.requests, "get", lambda *a, **k: _FakeResp({"_embedded": {"terms": []}}) + ) + rc = ol.main(["--id", "CL:9999999"]) + assert rc == 1 + assert "No term found" in capsys.readouterr().err + + +def test_main_no_results(monkeypatch, capsys): + monkeypatch.setattr( + ol.requests, "get", lambda *a, **k: _FakeResp({"response": {"docs": []}}) + ) + rc = ol.main(["zzznonsense"]) + assert rc == 1 + assert "No results" in capsys.readouterr().err + + +def test_main_no_args(capsys): + rc = ol.main([]) + assert rc == 2 + + +def test_main_network_error(monkeypatch, capsys): + def boom(*a, **k): + raise RuntimeError("connection timeout") + + monkeypatch.setattr(ol.requests, "get", boom) + rc = ol.main(["NK cell"]) + assert rc == 1 + assert "connection timeout" in capsys.readouterr().err + + +def test_main_rows_flag(monkeypatch): + captured = {} + + def fake_get(url, params, timeout): + captured["rows"] = params.get("rows") + return _FakeResp(_search_payload(_doc())) + + monkeypatch.setattr(ol.requests, "get", fake_get) + ol.main(["--rows", "10", "NK"]) + assert captured["rows"] == 10 + + +def test_main_ontology_flag(monkeypatch): + captured = {} + + def fake_get(url, params, timeout): + captured["ontology"] = params.get("ontology") + return _FakeResp(_search_payload(_doc())) + + monkeypatch.setattr(ol.requests, "get", fake_get) + ol.main(["--ontology", "uberon", "brain"]) + assert captured["ontology"] == "uberon" + + +def test_main_multiple_results(monkeypatch, capsys): + monkeypatch.setattr( + ol.requests, + "get", + lambda *a, **k: _FakeResp( + _search_payload( + _doc("CL:0000623", "natural killer cell"), + _doc("CL:0000825", "pro-natural killer cell"), + ) + ), + ) + rc = ol.main(["NK"]) + assert rc == 0 + out = capsys.readouterr().out + assert "Found 2 result" in out + assert "CL:0000623" in out + assert "CL:0000825" in out diff --git a/tests/test_report_validator.py b/tests/test_report_validator.py index 2d629f0..39e6721 100644 --- a/tests/test_report_validator.py +++ b/tests/test_report_validator.py @@ -96,7 +96,9 @@ def test_validate_report_reads_cache(tmp_path): sc.append_snippets("run", "q1", 1, "q", {"result": {"data": [item]}}, root=tmp_path) report = tmp_path / "report.md" - report.write_text(_report('> "Natural killer cells express CD56 at varying levels"')) + report.write_text( + _report('> "Natural killer cells express CD56 at varying levels"') + ) passed, errors = rv.validate_report(report, "run", "q1", root=tmp_path) assert passed and errors == [] @@ -114,26 +116,68 @@ def test_validate_report_no_cache_fails(tmp_path): # --------------------------------------------------------------------------- # def test_main_clean_exit0(tmp_path, capsys): sc.append_snippets( - "run", "q1", 1, "q", - {"data": [{"paper": {"corpusId": "1"}, "snippet": {"text": "hello world", "snippetOffset": {}}}]}, + "run", + "q1", + 1, + "q", + { + "data": [ + { + "paper": {"corpusId": "1"}, + "snippet": {"text": "hello world", "snippetOffset": {}}, + } + ] + }, root=tmp_path, ) report = tmp_path / "report.md" report.write_text(_report('> "hello world"')) - rc = rv.main(["--report", str(report), "--run", "run", "--qid", "q1", "--root", str(tmp_path)]) + rc = rv.main( + [ + "--report", + str(report), + "--run", + "run", + "--qid", + "q1", + "--root", + str(tmp_path), + ] + ) assert rc == 0 assert "OK" in capsys.readouterr().out def test_main_failure_exit1(tmp_path, capsys): sc.append_snippets( - "run", "q1", 1, "q", - {"data": [{"paper": {"corpusId": "1"}, "snippet": {"text": "hello world", "snippetOffset": {}}}]}, + "run", + "q1", + 1, + "q", + { + "data": [ + { + "paper": {"corpusId": "1"}, + "snippet": {"text": "hello world", "snippetOffset": {}}, + } + ] + }, root=tmp_path, ) report = tmp_path / "report.md" report.write_text(_report('> "fabricated"')) - rc = rv.main(["--report", str(report), "--run", "run", "--qid", "q1", "--root", str(tmp_path)]) + rc = rv.main( + [ + "--report", + str(report), + "--run", + "run", + "--qid", + "q1", + "--root", + str(tmp_path), + ] + ) assert rc == 1 assert "FAILED" in capsys.readouterr().out diff --git a/tests/test_snippet_cache.py b/tests/test_snippet_cache.py index 318f5cb..e3d014e 100644 --- a/tests/test_snippet_cache.py +++ b/tests/test_snippet_cache.py @@ -9,7 +9,9 @@ from soulcap_cl_mapping import snippet_cache as sc -def _data_item(corpus_id, text, *, start=0, end=100, refs=None, sentences=None, title="T"): +def _data_item( + corpus_id, text, *, start=0, end=100, refs=None, sentences=None, title="T" +): """Build one ASTA ``result.data`` item.""" return { "score": 0.9, @@ -92,8 +94,13 @@ def test_open_access_closed_is_false(): item = _data_item("1", "t") item["paper"]["openAccessInfo"] = {"status": "CLOSED"} rec = sc._record_from_data_item( - item, run_id="r", qid="q", round=1, query="q", - source_seed=None, followed_corpus_ids=None, + item, + run_id="r", + qid="q", + round=1, + query="q", + source_seed=None, + followed_corpus_ids=None, ) assert rec["open_access"] is False @@ -120,7 +127,9 @@ def test_load_questions_missing_returns_empty(tmp_path): # append / load # --------------------------------------------------------------------------- # def test_append_creates_jsonl_and_counts(tmp_path): - result = _result(_data_item("1", "alpha"), _data_item("2", "beta", start=200, end=300)) + result = _result( + _data_item("1", "alpha"), _data_item("2", "beta", start=200, end=300) + ) n = sc.append_snippets("run", "q1", 1, "query", result, root=tmp_path) assert n == 2 assert sc.question_file("run", "q1", tmp_path).exists() @@ -128,13 +137,17 @@ def test_append_creates_jsonl_and_counts(tmp_path): def test_append_empty_data_returns_zero(tmp_path): - n = sc.append_snippets("run", "q1", 1, "query", {"result": {"data": []}}, root=tmp_path) + n = sc.append_snippets( + "run", "q1", 1, "query", {"result": {"data": []}}, root=tmp_path + ) assert n == 0 assert sc.load_records("run", "q1", tmp_path) == [] def test_append_accepts_bare_data_shape(tmp_path): - n = sc.append_snippets("run", "q1", 1, "q", {"data": [_data_item("1", "x")]}, root=tmp_path) + n = sc.append_snippets( + "run", "q1", 1, "q", {"data": [_data_item("1", "x")]}, root=tmp_path + ) assert n == 1 @@ -148,26 +161,44 @@ def test_append_dedup_same_offset(tmp_path): def test_multi_round_appends_distinct(tmp_path): - sc.append_snippets("run", "q1", 1, "q", _result(_data_item("1", "a")), root=tmp_path) sc.append_snippets( - "run", "q1", 2, "q", _result(_data_item("2", "b", start=5, end=9)), root=tmp_path + "run", "q1", 1, "q", _result(_data_item("1", "a")), root=tmp_path + ) + sc.append_snippets( + "run", + "q1", + 2, + "q", + _result(_data_item("2", "b", start=5, end=9)), + root=tmp_path, ) records = sc.load_records("run", "q1", tmp_path) assert [r["round"] for r in records] == [1, 2] def test_multi_question_isolation(tmp_path): - sc.append_snippets("run", "q1", 1, "q", _result(_data_item("1", "a")), root=tmp_path) - sc.append_snippets("run", "q2", 1, "q", _result(_data_item("2", "b")), root=tmp_path) + sc.append_snippets( + "run", "q1", 1, "q", _result(_data_item("1", "a")), root=tmp_path + ) + sc.append_snippets( + "run", "q2", 1, "q", _result(_data_item("2", "b")), root=tmp_path + ) assert sc.load_snippet_texts("run", "q1", tmp_path) == ["a"] assert sc.load_snippet_texts("run", "q2", tmp_path) == ["b"] def test_followed_corpus_ids_recorded(tmp_path): - item = _data_item("100", "txt", refs=[{"start": 1, "end": 2, "matchedPaperCorpusId": "200"}]) + item = _data_item( + "100", "txt", refs=[{"start": 1, "end": 2, "matchedPaperCorpusId": "200"}] + ) sc.append_snippets( - "run", "q1", 1, "q", _result(item), - followed_corpus_ids={"100": ["200"]}, root=tmp_path, + "run", + "q1", + 1, + "q", + _result(item), + followed_corpus_ids={"100": ["200"]}, + root=tmp_path, ) rec = sc.load_records("run", "q1", tmp_path)[0] assert rec["followed_corpus_ids"] == ["200"] @@ -207,7 +238,9 @@ def test_default_root_falls_back_to_repo(monkeypatch): def test_main_init(tmp_path, capsys): qfile = tmp_path / "questions.json" qfile.write_text(json.dumps([{"qid": "q1", "text": "?", "seeds": []}])) - rc = sc.main(["--root", str(tmp_path), "init", "--run", "r", "--questions", str(qfile)]) + rc = sc.main( + ["--root", str(tmp_path), "init", "--run", "r", "--questions", str(qfile)] + ) assert rc == 0 assert "initialised" in capsys.readouterr().out assert (tmp_path / "r" / "questions.json").exists() @@ -220,8 +253,19 @@ def test_main_append_from_stdin(tmp_path, capsys, monkeypatch): monkeypatch.setattr("sys.stdin", io.StringIO(payload)) rc = sc.main( [ - "--root", str(tmp_path), "append", "--run", "r", "--qid", "q1", - "--round", "1", "--query", "q", "--result", "-", + "--root", + str(tmp_path), + "append", + "--run", + "r", + "--qid", + "q1", + "--round", + "1", + "--query", + "q", + "--result", + "-", ] ) assert rc == 0 @@ -234,9 +278,23 @@ def test_main_append_from_file(tmp_path): rfile.write_text(json.dumps(_result(_data_item("1", "x")))) rc = sc.main( [ - "--root", str(tmp_path), "append", "--run", "r", "--qid", "q1", - "--round", "1", "--query", "q", "--result", str(rfile), - "--source-seed", "DOI:10.1/x", "--followed", "1=2", + "--root", + str(tmp_path), + "append", + "--run", + "r", + "--qid", + "q1", + "--round", + "1", + "--query", + "q", + "--result", + str(rfile), + "--source-seed", + "DOI:10.1/x", + "--followed", + "1=2", ] ) assert rc == 0 @@ -244,7 +302,14 @@ def test_main_append_from_file(tmp_path): def test_main_show(tmp_path, capsys): - sc.append_snippets("r", "q1", 1, "q", _result(_data_item("1", "x", title="My Paper")), root=tmp_path) + sc.append_snippets( + "r", + "q1", + 1, + "q", + _result(_data_item("1", "x", title="My Paper")), + root=tmp_path, + ) rc = sc.main(["--root", str(tmp_path), "show", "--run", "r", "--qid", "q1"]) assert rc == 0 out = capsys.readouterr().out diff --git a/tests/test_sync_sheets.py b/tests/test_sync_sheets.py index d6bce04..10078fa 100644 --- a/tests/test_sync_sheets.py +++ b/tests/test_sync_sheets.py @@ -12,9 +12,7 @@ from soulcap_cl_mapping import sync_sheets as ss -XLSX_CONTENT_TYPE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" -) +XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" # --------------------------------------------------------------------------- # @@ -32,9 +30,9 @@ def _make_workbook(path: Path) -> None: } ).to_excel(xw, sheet_name="Marker Combinations", index=False) # Layout sheet (no real header): fully-empty col should be dropped. - pd.DataFrame( - [["Species", "Human", None], [None, None, None]] - ).to_excel(xw, sheet_name="Global", index=False, header=False) + pd.DataFrame([["Species", "Human", None], [None, None, None]]).to_excel( + xw, sheet_name="Global", index=False, header=False + ) # Empty sheet: should be skipped. pd.DataFrame().to_excel(xw, sheet_name="Empty", index=False, header=False) @@ -204,9 +202,9 @@ def fake_sync(sheet_id, data_dir, write_csv): def test_main_uses_sheet_id_env(monkeypatch, tmp_path): captured = {} monkeypatch.setattr( - ss, "sync", lambda sheet_id, data_dir, write_csv: captured.update( - sheet_id=sheet_id - ) + ss, + "sync", + lambda sheet_id, data_dir, write_csv: captured.update(sheet_id=sheet_id), ) monkeypatch.setenv("SOULCAP_SHEET_ID", "FROM_ENV") assert ss.main(["--data-dir", str(tmp_path)]) == 0 @@ -216,9 +214,9 @@ def test_main_uses_sheet_id_env(monkeypatch, tmp_path): def test_main_flag_overrides_env(monkeypatch, tmp_path): captured = {} monkeypatch.setattr( - ss, "sync", lambda sheet_id, data_dir, write_csv: captured.update( - sheet_id=sheet_id - ) + ss, + "sync", + lambda sheet_id, data_dir, write_csv: captured.update(sheet_id=sheet_id), ) monkeypatch.setenv("SOULCAP_SHEET_ID", "FROM_ENV") ss.main(["--sheet-id", "FROM_FLAG", "--data-dir", str(tmp_path)]) diff --git a/uv.lock b/uv.lock index c347339..bce4d3c 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version < '3.12' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -13,6 +16,47 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] +[[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 = "certifi" version = "2026.6.17" @@ -236,6 +280,141 @@ 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.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/e0/dbd0f2a68a1c1a1991eb7921ff6014465d56608cdc9a9fb468a616210a37/librt-0.12.0.tar.gz", hash = "sha256:cb26faedbd09c6130e9c1b64d8000efec5076ffd18d606c6cd1cf02730e6d8b0", size = 203841, upload-time = "2026-06-30T16:14:29.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/ab/628490f42d1eba82f3c7e5821aa62013e6df7f525b7a9e92c048f8d1cc1c/librt-0.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f13c1e8563102c2b17581cf37fcb2c6dae7ad485ccea93ae46258998c25f9a1", size = 143821, upload-time = "2026-06-30T16:12:23.248Z" }, + { url = "https://files.pythonhosted.org/packages/38/5f/793e8b6f4b6ac16e7d7198478c0af3670606fbb535c768d5f3e954781423/librt-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1ddff067610a122387024c4df527493b909d41e54a6e5b2d0e6c1041d6dfa09", size = 148442, upload-time = "2026-06-30T16:12:24.582Z" }, + { url = "https://files.pythonhosted.org/packages/ad/92/c780fe37a9e0982f3bd8fd9a631d6b95d09a5a7201c6c50366ce843b7e42/librt-0.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8dc7ebb5f3eec062398e9d0ef1938acd21b589e74286c4a8906d0183318d91b", size = 478276, upload-time = "2026-06-30T16:12:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/41/bb/226d444bc20d7dff4a19ec6c1ff2c13a76385eebddb59c9c00c923b67536/librt-0.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:198de569ea9d5f6f33808f1c00cc3db9de62bf4d6deafa3b052bd08255083038", size = 472337, upload-time = "2026-06-30T16:12:27.83Z" }, + { url = "https://files.pythonhosted.org/packages/12/79/98ac0840ee90a75d4e1155c79062860b12ccca508587ff2119fc086965f2/librt-0.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e958678a8bca56016aedc891b391c0e0813ea382a874b54a2c1b313c1d232720", size = 502087, upload-time = "2026-06-30T16:12:29.443Z" }, + { url = "https://files.pythonhosted.org/packages/6f/72/a6b1a0d080606a7f5f646b79a1496f21d709f8563877759ace9ce5adad73/librt-0.12.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575a6eca68c8437ed4a8e0f534e31d74b562ba1049a0ee4b5f09e114bcc21be1", size = 493202, upload-time = "2026-06-30T16:12:31.077Z" }, + { url = "https://files.pythonhosted.org/packages/69/cf/e1b036b45f2fc272205ee18bf272b47e8d684bf1a75af26db440c7504359/librt-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:86f241c50dc9e9a3f0db6dbb37a607c8205aa87b920802dabbd50b70d40f6939", size = 514139, upload-time = "2026-06-30T16:12:33.032Z" }, + { url = "https://files.pythonhosted.org/packages/40/34/b193b3e6985469a2f8afa86c90012329c86480b6ff4f2e4bd7b5b937e134/librt-0.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:113417b934fbf38220a9c7fe94578cefbe7dbb047adcb75aa197905af2b13724", size = 519486, upload-time = "2026-06-30T16:12:34.996Z" }, + { url = "https://files.pythonhosted.org/packages/31/9e/7de4947b1695f247c813f833e3c1e7b77b52e52a7dba2c35411cf806b58e/librt-0.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:762f17c0eb6b5d74e269126996cea8a89e35ab6464c5151619163abcd8623ae2", size = 499609, upload-time = "2026-06-30T16:12:36.663Z" }, + { url = "https://files.pythonhosted.org/packages/59/11/f3730e04e758b1fbf215359062ad2d5b6bd0b0ab5ac46b1c140628795be7/librt-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa93b3bd7f7588c628f6e9bf66485d3467fd9a1ccdb8975b770178f39f35697", size = 542205, upload-time = "2026-06-30T16:12:38.56Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/710453617eabe20e18433864f335534c8aff63fbc68d8cd9dbc70a3d08f6/librt-0.12.0-cp311-cp311-win32.whl", hash = "sha256:aaa04b44d4fe86d824616b1f9c13e34c7c01ec0c96dd2abc4f59423696f788e2", size = 98067, upload-time = "2026-06-30T16:12:40.102Z" }, + { url = "https://files.pythonhosted.org/packages/42/53/401bff50a56e95daf151d911c99adf5732af2190e8f4d11886c9a229103c/librt-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:9aaeeddb8e7e4ae3bb9f944e0e618418cb91c0071d5ddbfcc3584b3cf59d39f0", size = 118346, upload-time = "2026-06-30T16:12:41.388Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9a/a3a9078fe88bfc2d2d99dcf1c18593938ae830089cf84c3b2532a6c49d63/librt-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:18a2402fa3123ab76ecca670e6fb33038fde7c1e91181b885226ec4d30af2c2c", size = 104760, upload-time = "2026-06-30T16:12:43.112Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1a/5bec493821b0e85b91de4f234912b50133d1aedb875048eef27938ec3f96/librt-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9bce19aa7c05f91c989f9da7b567f81d21d57a2e6501e2b811aa0f3f79614c1a", size = 146756, upload-time = "2026-06-30T16:12:44.395Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d0/cc04b48a57c1f275387f5578847214c4a6c21bfb24c6c8c8d6ba753fe403/librt-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0ace09f5bf4d982fe726015f102fb856658b41580597104e301e630ed1d8d86", size = 145537, upload-time = "2026-06-30T16:12:45.95Z" }, + { url = "https://files.pythonhosted.org/packages/9e/10/c02325556beb2aa158c9e549ddade8cc9a23b36cdad14756dbed730c1ff1/librt-0.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d007efe9243ede81ce75990ad7aa172da1e2024144b3eff17ba46a5fff1fff3c", size = 488637, upload-time = "2026-06-30T16:12:47.658Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9e/7b49ca1c30baa9c8df96024aa09a97c35a97455e36004c9b5311703c56f3/librt-0.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ad324a5e4858388a4864915b90a42efc8b374376393f14b9940f2454e791912b", size = 483651, upload-time = "2026-06-30T16:12:49.283Z" }, + { url = "https://files.pythonhosted.org/packages/4d/71/03c8c8cec39645fda451132ff9d6d662fc5aea42a1a188a77a4fddb35906/librt-0.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10a40cf74cdd97b6f8f905056db73f5d459783de2ca04c6ebd1bf47652818e7e", size = 518359, upload-time = "2026-06-30T16:12:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ec/a9f357f94bbcba92277d22af22cff42ef706ae5d9d6d58b69bebf3a67954/librt-0.12.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:92e61c09de95217ae02a9d17f4f66cf073253cdc51bcfdc0f15c62c9a70baa85", size = 509510, upload-time = "2026-06-30T16:12:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/717055325d028743aa01a7691ad59a63352a26a8ff2e7eeb0c9249514150/librt-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0461344061d6fc3718940f5855d95647831cef6d03a6c7506897f98222784ad4", size = 527302, upload-time = "2026-06-30T16:12:54.244Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/7612eeedb3395d92f7c6a84dca5f15e282d650483a4dc01aa5b9cffdfda3/librt-0.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e6dfe89074732c9287b3c0f5a6af575c9ede380a788013876cc7b14fe0da0361", size = 532568, upload-time = "2026-06-30T16:12:55.74Z" }, + { url = "https://files.pythonhosted.org/packages/79/1e/a9afe85d5bb8b65dc27be3809ed1d69082079e1e9717fd2c66aa9939600c/librt-0.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9efed79d51ad1383bba0855f613cca7aa91c943e709af2413ac7f4bb9936ce08", size = 521579, upload-time = "2026-06-30T16:12:57.884Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/93aebb219d52c37ea578f83b0588cd7b040974e464d4e435086a48b4dc4d/librt-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1eac6cc0e23e448fb3c1446ed85ff796afb616eed5897c978d35dbec030b7c7c", size = 558743, upload-time = "2026-06-30T16:12:59.577Z" }, + { url = "https://files.pythonhosted.org/packages/3c/85/1680c0ec332f238e3145c5608d313ab0a43281e210a5dd87e3bc3cc25631/librt-0.12.0-cp312-cp312-win32.whl", hash = "sha256:0ab8ee0210047ae86ca023ccfbfe3df82077fd1c9bc021aebbf37d993ef64af0", size = 99200, upload-time = "2026-06-30T16:13:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/30/0e/abca12d8904875aa2ad66327390a3f7b1b75ebc43c0a00fc763cecf32ea5/librt-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:51c8bfa12632c81b94401c101bcedd0c56c3a1f8fa3273ca3472b28cd2f54003", size = 119390, upload-time = "2026-06-30T16:13:02.493Z" }, + { url = "https://files.pythonhosted.org/packages/32/a5/4203481b6d3a3bb348c82ac71abf1fcb4cb3ae8422a24a8dee4cd3ac5bd7/librt-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:5eebd451f5def089369ba6d8ff0291303d035e8154f9f26f7633835c5b029ade", size = 105117, upload-time = "2026-06-30T16:13:03.952Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/568d948c8079c9ff3c9e8110cf85f1eb70218e1209af29d0b7b89aa4a60c/librt-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d9a55760a34ae5ce70434aabb6a6c61c6c44a0ec58ca1cfd9cd86e4745d417d", size = 146808, upload-time = "2026-06-30T16:13:05.417Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/bea471ecea210088847bb5f3c4b4b424d596518934c06679b78ca85d6e63/librt-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff0b197e338b4cf432873e0d6ef025213fdea85311ec4d87d2ea88c28adf2409", size = 145503, upload-time = "2026-06-30T16:13:07.023Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9e/984ad422b56de95fdce158f06b051655373784ebea0aba9a7fcbc41614d1/librt-0.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e69f120a20b69e2539d603bbd4d62db38399b10f8bf73a1cf445038a621e8af", size = 488421, upload-time = "2026-06-30T16:13:08.492Z" }, + { url = "https://files.pythonhosted.org/packages/50/03/1a2f94009b07ea71f8e1a4cfe53370565b56da9caa341b89e0699325e9f5/librt-0.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fde3cde595e947fc8e755b0a21f919a1622483d07c662d00496e040773d22591", size = 483488, upload-time = "2026-06-30T16:13:10.169Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3b/084bdc295823fbb6ab91670047adf8f420787f9e8794bf2d140b66dc196b/librt-0.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d977447315fa09ea4e8c7ae9b4e22f7659b5128161c1fd55ff786b5349f73503", size = 518428, upload-time = "2026-06-30T16:13:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/c9/22/5a307390b93a115ffbecd95c64eecb4e56269680e45e9415ada7285f2cf4/librt-0.12.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ffac8a67e4143cea9a549d4822b93bc0bbaad73fc25aa0ab0ba5ec27d178677", size = 509744, upload-time = "2026-06-30T16:13:13.217Z" }, + { url = "https://files.pythonhosted.org/packages/b5/90/83f3cb6184f5d669660717b4b2e317c9ddaccf7ca5bb97f2196deac1a3b7/librt-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:94af1ed773ff104ef08ef3d669a0ba9d3a5916c609eb698cffe5d5476d66ff9b", size = 527749, upload-time = "2026-06-30T16:13:15.277Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3b/f162be5cc88d47378e3a20776fe425fa1c2bece755da15e2783ebf06d3d6/librt-0.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:548199d21d22fb26398dfbbe0ba953a52465c66f3a49f38e6fddce1b127faf53", size = 532582, upload-time = "2026-06-30T16:13:17.074Z" }, + { url = "https://files.pythonhosted.org/packages/c9/28/6c5d2f6b7232fd24f284fc4cab37a459fe69a9096a09942f44cc5c55e073/librt-0.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c8f1f413b966a9dd3ecf80cd337b0ad7bb3de2474a4ff448ed3ebabfc3f803fc", size = 522235, upload-time = "2026-06-30T16:13:18.823Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1c/bd115360587fdc22c8ae8fac14c040a556b442e2965d4370d2cf274c8b95/librt-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55f13f95b629be5b6ab38918e439bf14169d6f9a8deaae55e0c14e12fb0c74b9", size = 559055, upload-time = "2026-06-30T16:13:20.509Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5a/c26f49f576437014825a86faea3cec60c1ed17f976abd567b6c12b8e35a7/librt-0.12.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:8b2dc079dfe29e77a47a19073d2040fa4879aa3656501f1650f8402ddce0313c", size = 79809, upload-time = "2026-06-30T16:13:22.401Z" }, + { url = "https://files.pythonhosted.org/packages/69/0b/a55244261d9ad7375ac039b8af06d42602722e2e8b8d8d6b86e4a3888c02/librt-0.12.0-cp313-cp313-win32.whl", hash = "sha256:da58944be8270f2bfee628a9a2a60c1cf6a12c8bea8e2c9b6edf3e5414ca7793", size = 99308, upload-time = "2026-06-30T16:13:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bf/ed9465e58d44c5a5637795547d0841c8934aab905ea452cac1adf14672cf/librt-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:1db4be3037e4ce065a071fa7deee93e78ebc25f448340a02a6c1c0b82c37e383", size = 119438, upload-time = "2026-06-30T16:13:25.188Z" }, + { url = "https://files.pythonhosted.org/packages/c0/44/3cad652aeb892e6e8ffe48d0fafa2bc652f28ec7ed3f4403fcbb1be4f948/librt-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:05fd2542892ad770b5dd45003fd080477cf220b611d3ee59b0792097eb0873a9", size = 105118, upload-time = "2026-06-30T16:13:26.533Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/3a0e05618c12423b6fc5141b590ec02a6efb645833edc8736a6c7b46d1ec/librt-0.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b37ee42e09722284a6d9288fe44a191f7276060a3195939bb77c6502058dbb34", size = 145579, upload-time = "2026-06-30T16:13:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/77/9e/fd399d099dfb4020f3f7c34e7e6210c389fa89f7d79ca92f5afb0395f278/librt-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ade11988728b3e4768dadc5696e82c60e9b35fc95335a9b4d1f5d69e753ccec7", size = 150139, upload-time = "2026-06-30T16:13:29.357Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ee/610239fbd8c4b005443664c5d4c3bc1717daedd8c71369bf45011aa87194/librt-0.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f351ed425380e39bd86df382578aa5b8c5b98e2e265112de7379e7d030258150", size = 480457, upload-time = "2026-06-30T16:13:30.78Z" }, + { url = "https://files.pythonhosted.org/packages/0c/10/ceddc9010f26c541444be36e1153a79b64626694db2d33a524c719fa3e46/librt-0.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:857d2163e088c868967717ace8e980017fd868a735f3de010412af02bdc30319", size = 479002, upload-time = "2026-06-30T16:13:32.398Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/b1523d9718e8192e5403e6b41a02742e17ba554369f0729b9f30ab590e2d/librt-0.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2befc80aa5f2f5b93f28abaaf11feff6677931dd548320e44c52deaa9399744", size = 510527, upload-time = "2026-06-30T16:13:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0e/0f3ff43befb18a531615736791e52fb67eaa71ff7b89e6e5f7004b64cc6e/librt-0.12.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:be3694dcfa97c6715dd19ac73d3e1b21a805514a5785663e57fecacd3ff64e5a", size = 500988, upload-time = "2026-06-30T16:13:36.408Z" }, + { url = "https://files.pythonhosted.org/packages/a8/1a/0278ea4a9e599dc507c43839a87f2c764ad04bf69418e2d763d58659e55f/librt-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2d5f67e86f45638843d025b0828f2e9e55fc45ff9180d2618ccdeaf72a796050", size = 519318, upload-time = "2026-06-30T16:13:37.883Z" }, + { url = "https://files.pythonhosted.org/packages/59/55/090e10e62be2f35265e41601337f83ac9f83be9aca1bf92692e3a82effdd/librt-0.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:64572c85e4ab7d572c9b72cd76b5f90b21181b1459fa6b1aac6f8958c4fcff31", size = 527127, upload-time = "2026-06-30T16:13:39.682Z" }, + { url = "https://files.pythonhosted.org/packages/1f/34/8052c9ec678be6ba751279947831f089aa69b009000b985ce91d1979669a/librt-0.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8b961912b0e688c1eb4658a46bdb0606b31918d65597fbe7356ca83aa653ffcc", size = 509766, upload-time = "2026-06-30T16:13:41.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f8/8761b36189e9ec8dc20b49fa84cef22852c6c41fcda56f760f7fc1360da5/librt-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:722375903e3f079436a7a33da51ce73931536dd041f9feb01536f05d8e010c96", size = 552043, upload-time = "2026-06-30T16:13:43.197Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/7283971ef6b70269938b49c7b25f670ec6325d252265fbcc996f9b364379/librt-0.12.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a5a96a8f536b65ef1bf910c09e7e71647edde5111f6e1b51f413c6fba5bfe71b", size = 79472, upload-time = "2026-06-30T16:13:44.64Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5e/b30940dea935e8ac5bd0e0abb1985f5274590d557ac3a252ca0d5392ce52/librt-0.12.0-cp314-cp314-win32.whl", hash = "sha256:8ffc99c356f1777c506e1b69dc303879153ae2640ba15b8f3d4448bc87139149", size = 94246, upload-time = "2026-06-30T16:13:45.962Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4e/0af9fe63f35fa304da3b05688f30ff6a329bcc59581b1cc51dc87fd30141/librt-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:1e68fb20798f455cda41d20a306a23c901218883f17a4bab1ed6e1331b265fb7", size = 114951, upload-time = "2026-06-30T16:13:47.279Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8e/843c495d7db35e13b84cd533898fa89145c40dc255da0bc316d53d631464/librt-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:2df534f97916cf38ec9b1ddafeb68ae1a4cd4a54775ff26a797026774c0517cf", size = 100562, upload-time = "2026-06-30T16:13:48.699Z" }, + { url = "https://files.pythonhosted.org/packages/75/30/c686d0f978d5fd6867c5bbad96b015c9445746764d1c228e16a2d30d9382/librt-0.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c09e581b1c2b8a62b809d4f4bd101ca3de93791e5b0ed1a14085d911be3dee3f", size = 153897, upload-time = "2026-06-30T16:13:50.017Z" }, + { url = "https://files.pythonhosted.org/packages/40/46/f6f2d77ce46628b48fb5280709013b5109cf3a2c46a2472093cdfc03519d/librt-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:976888d0d831402086e641018bcc3208e0a38f0835789da91f72894b2cb4161f", size = 156391, upload-time = "2026-06-30T16:13:51.462Z" }, + { url = "https://files.pythonhosted.org/packages/c2/46/cd790c7e19e460779471530ffab454541d6ea4a3b7d338cad7f16ff96995/librt-0.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563c37cdb41d08fe1e3f08b201abac0e317ca18e88b91285466ee0a585797520", size = 564151, upload-time = "2026-06-30T16:13:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/724559a15fb023cbdef7aee1e81fbfbc3ee22fd09009baa816cea63e3a60/librt-0.12.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b97eb1a3140e279cc76f85b0fb92b7eb3dfbe0471260ee878bc9dc4bf9a0d649", size = 546002, upload-time = "2026-06-30T16:13:54.665Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7e/f9d8c257ab4909f101c7c13734367749e782fd8625545f0343502c2f09f1/librt-0.12.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06e0623351ab9904cf628245f99c714586f4dd23dc740b88c8bc670d8401a847", size = 584204, upload-time = "2026-06-30T16:13:56.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/33/64665810575ac23b6cb6ef364de51309b7803620c12885b6e895ebc29591/librt-0.12.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da12f017b2e404554be14d466cd992459feaa44f252b0f18d909a85266ce1237", size = 573688, upload-time = "2026-06-30T16:13:58.1Z" }, + { url = "https://files.pythonhosted.org/packages/0f/01/27522995c6627455abc7a939d57535fb1a7836d398ccedb3d7585f46039e/librt-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d97f31003a5c86b9e78155a829572c3a26484064fb7ac1d9695fe628bd93d029", size = 604719, upload-time = "2026-06-30T16:13:59.831Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1f/099e61b1b688551d6d2ce9d4d2ae2242a938759db8551e6cbac7f7176ee5/librt-0.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:bd43a6c69876aef4f04eaae3d3b99b0be64755fda274002fa445b92480bf664e", size = 598183, upload-time = "2026-06-30T16:14:01.457Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c1/050400249665503bdd5b83cec518fa7b183b609341c8dcd58161775c4226/librt-0.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c01755c72fca1dc6b8d5c2ed228b8e7b2ffe184675c22f0f05ebd8fe188b9250", size = 582559, upload-time = "2026-06-30T16:14:03.29Z" }, + { url = "https://files.pythonhosted.org/packages/da/d1/eef8f0e6722518b65a3d3bcd9309f9f44e208ce5d6728070820f988e7078/librt-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:625ae561d5fa36400856dcc27464400d047bc2d5e3446be88f437b03fefd72e4", size = 626375, upload-time = "2026-06-30T16:14:04.957Z" }, + { url = "https://files.pythonhosted.org/packages/8b/78/f0bb41a6f2bbd3c77bdcc66980dc0d69ca1192a0ecec25377afcc5e6db73/librt-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8d73191883553ee0739741544bf3b00aba2a1224e45d9580b30cbc29e21dc03b", size = 97752, upload-time = "2026-06-30T16:14:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/92/24/e279c27972ab051a070237cfa45728fa51670c3f22f1a4d391711e9f4c31/librt-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e1cbb037324e759f0afa270229731ff0047772667f3cb38ef5df2cabf0175ede", size = 119562, upload-time = "2026-06-30T16:14:07.908Z" }, + { url = "https://files.pythonhosted.org/packages/06/e6/42a475bfca683b0cd5366f6dd06580062b7e567bb8534d225c877c2f14f3/librt-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bca1472acbd473eff61059b4409f802c5a1bcb4cd0344d06f939df9c4c125d40", size = 104282, upload-time = "2026-06-30T16:14:09.29Z" }, +] + +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "numpy" version = "2.4.6" @@ -325,9 +504,12 @@ name = "numpy" version = "2.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", @@ -461,6 +643,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, ] +[[package]] +name = "pandas-stubs" +version = "3.0.3.260530" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/aa/c41a8a0ff86fd85dbb3ec0c1f3fa488ca64a8b5f82654ae1b07d84acefe5/pandas_stubs-3.0.3.260530.tar.gz", hash = "sha256:d1efe47b2e5a312c047d7feabec5cb7a55365747983420077e9fcbe9ab74f714", size = 113183, upload-time = "2026-05-30T17:47:40.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e0/99ec5b02203c4e9ce878bc63d8caa06ac1f891e4d63bded9a5ced70fcb4f/pandas_stubs-3.0.3.260530-py3-none-any.whl", hash = "sha256:a6277eb1c8cebf48d9b2413fcd2e9a6b4ff479c934a223c29eacbc3058c4cb55", size = 173780, upload-time = "2026-05-30T17:47:39.13Z" }, +] + +[[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" @@ -615,6 +819,8 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "mypy" }, + { name = "pandas-stubs" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, @@ -623,8 +829,10 @@ dev = [ [package.metadata] requires-dist = [ { name = "certifi", specifier = ">=2024.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" }, { name = "openpyxl", specifier = ">=3.1" }, { name = "pandas", specifier = ">=2.2" }, + { name = "pandas-stubs", marker = "extra == 'dev'", specifier = ">=2.2" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" }, { name = "python-dotenv", specifier = ">=1.0" }, @@ -700,6 +908,15 @@ wheels = [ { 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" }, +] + [[package]] name = "tzdata" version = "2026.2"