From 073c033c7d24829d8b3b8337b5de8f6b3fe8f80d Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Fri, 13 Feb 2026 23:25:17 +0000 Subject: [PATCH 01/38] docs: add dependency modernization implementation plan --- DEPENDENCY_MODERNIZATION_PLAN.md | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 DEPENDENCY_MODERNIZATION_PLAN.md diff --git a/DEPENDENCY_MODERNIZATION_PLAN.md b/DEPENDENCY_MODERNIZATION_PLAN.md new file mode 100644 index 0000000..022c14e --- /dev/null +++ b/DEPENDENCY_MODERNIZATION_PLAN.md @@ -0,0 +1,59 @@ +# Dependency Modernization With Zero-Skip Test Policy + +## Summary + +- Treat `AGENTS.md` as policy-enforcing documentation for engineering agents. +- Make dependency management current (`uv` + single source of truth). +- Keep behavior stable while upgrading packages and model artifacts. +- Require all collected tests to pass with fully provisioned dependencies. + +## Scope + +### In + +- Add explicit “do not skip/disable tests” policy to `AGENTS.md`. +- Consolidate dependency definitions into one modern packaging workflow. +- Make full test execution deterministic, including currently optional integrations. +- Introduce a controlled model-improvement pipeline with measurable acceptance gates. + +### Out + +- Feature-level extraction redesign unrelated to dependency/model reliability. +- Permanent acceptance of partial-pass builds. + +## Action Items + +- [ ] Add a **Test Integrity Policy** section to `AGENTS.md` that forbids adding/removing `skip`, `skipif`, or `xfail` to bypass failures, and requires fixing root causes instead. +- [ ] Define a **100% pass target** as: all collected tests pass on a fully provisioned runner, including Stanford-gated tests when required assets are present. +- [ ] Add a **skip-audit check** in CI that fails if new skip/xfail markers are introduced without an approved issue link and expiry date. +- [ ] Consolidate packaging to `pyproject.toml` + lockfile and deprecate conflicting manifests (`Pipfile`, split requirements variants) after parity is captured. +- [ ] Standardize runtime on modern Python (default 3.11) and align metadata/docs/CI to that policy. +- [ ] Replace brittle setup scripts with deterministic bootstrap steps for NLTK corpora, contract pipeline artifacts, Java/Stanford assets, and optional Tika. +- [ ] Run compatibility validation for serialized ML pipelines against upgraded `scikit-learn`; retrain/re-export artifacts when incompatible, with explicit version tags. +- [ ] Add a model quality gate: compare old vs new models on fixed evaluation fixtures and accept upgrades only when metrics improve or regressions are within strict tolerance. +- [ ] Publish a migration runbook in repo docs with exact commands for local setup, full test run, optional component enablement, and failure triage. +- [ ] Roll out in staged PRs (policy/doc first, packaging second, CI third, model upgrades fourth), each required to stay green end-to-end. + +## Important Changes to Interfaces + +- Installation interface becomes `uv` + `pyproject.toml` driven. +- Dependency groups become explicit extras (`dev`, `test`, `stanford`, `tika`). +- Model artifact interface becomes versioned and benchmark-gated (new tags for improved models). +- No intended changes to user-facing extraction function signatures during dependency modernization. + +## Test Scenarios + +- Fresh environment bootstrap succeeds with documented commands only. +- Base suite passes with zero unexpected skips/failures. +- Stanford suite passes when provisioned and enabled. +- CI skip-audit catches any new bypass markers. +- Model regression suite compares baseline vs upgraded outputs and blocks degradations. +- Built wheel installs in clean venv and passes smoke tests. + +## Assumptions and Defaults + +- Default judgment: this should be done now. +- Default policy: no test disabling for convenience; failures are fixed, not hidden. +- Default Python target: 3.11 (with compatibility checks for adjacent supported versions). +- Default model policy: prefer newer models/methods only when measured outputs are better. +- Default blocker handling: if an external dependency outage prevents completion, build is marked blocked/failing with explicit root-cause notes, not treated as pass. From 1fa2d9f27d8659153ea3333b0c4c851e6e0dfbce Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 00:38:21 +0000 Subject: [PATCH 02/38] build: modernize packaging, CI, and zero-skip test policy --- .github/workflows/ci.yml | 110 ++ AGENTS.md | 164 +++ MIGRATION_RUNBOOK.md | 98 ++ README.md | 36 +- ci/skip_audit.py | 257 +++++ ci/skip_audit_allowlist.txt | 13 + lexnlp/extract/es/regulations.py | 15 +- lexnlp/ml/predictor.py | 20 + pyproject.toml | 98 ++ scripts/bootstrap_assets.py | 345 ++++++ scripts/model_quality_gate.py | 188 ++++ setup.py | 154 +-- uv.lock | 1696 ++++++++++++++++++++++++++++++ 13 files changed, 3040 insertions(+), 154 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 AGENTS.md create mode 100644 MIGRATION_RUNBOOK.md create mode 100644 ci/skip_audit.py create mode 100644 ci/skip_audit_allowlist.txt create mode 100644 pyproject.toml create mode 100755 scripts/bootstrap_assets.py create mode 100644 scripts/model_quality_gate.py mode change 100755 => 100644 setup.py create mode 100644 uv.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0f4e827 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,110 @@ +name: CI + +on: + pull_request: + push: + +permissions: + contents: read + +env: + PYTHON_VERSION: "3.11" + +jobs: + base-tests: + name: Base Tests + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Install dependencies + run: | + uv venv .venv --python "${PYTHON_VERSION}" + uv sync --frozen --python .venv/bin/python --extra dev --extra test + + - name: Bootstrap required assets + run: .venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model + + - name: Enforce skip-audit policy + run: .venv/bin/python ci/skip_audit.py + + - name: Run base suite + run: .venv/bin/pytest lexnlp + + stanford-tests: + name: Stanford Tests + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "11" + + - name: Install dependencies + run: | + uv venv .venv --python "${PYTHON_VERSION}" + uv sync --frozen --python .venv/bin/python --extra dev --extra test + + - name: Bootstrap required assets (including Stanford) + run: .venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model --stanford + + - name: Run Stanford suite + env: + LEXNLP_USE_STANFORD: "true" + run: | + .venv/bin/pytest \ + lexnlp/nlp/en/tests/test_stanford.py \ + lexnlp/extract/en/entities/tests/test_stanford_ner.py + + packaging-smoke: + name: Packaging Smoke + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Build source and wheel artifacts + run: | + uv venv .venv-build --python "${PYTHON_VERSION}" + uv build + + - name: Install wheel in clean env + run: | + uv venv .venv-smoke --python "${PYTHON_VERSION}" + uv pip install --python .venv-smoke/bin/python dist/*.whl + .venv-smoke/bin/python - <<'PY' + import lexnlp + print(getattr(lexnlp, "__version__", "unknown")) + PY diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9ccf2f3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,164 @@ +# AGENTS.md + +This document is a quick-start guide for coding agents working in this repository. + +## Project Summary + +- Project: `lexpredict-lexnlp` (LexNLP) +- Purpose: legal-text NLP and information extraction library +- Primary package: `lexnlp/` +- Packaging: `pyproject.toml` (setuptools backend; version in repo: `2.3.0`) +- Python requirement in `pyproject.toml`: `>=3.10,<3.13` (default to Python `3.11`) + +## Directory Structure + +```text +. +|-- lexnlp/ # Main package +| |-- config/ # Locale-specific configuration (en, de, es) +| |-- extract/ # Extraction modules by locale and domain +| | |-- common/ +| | |-- en/ +| | |-- de/ +| | |-- es/ +| | `-- ml/ +| |-- ml/ # ML utilities/catalog helpers +| |-- nlp/ # NLP components and training helpers +| |-- tests/ # Shared test helpers + tests +| `-- utils/ # Utility modules and utility tests +|-- test_data/ # Fixtures, sample inputs, expected outputs +|-- scripts/ # Helper scripts (Tika, release, data helpers) +|-- libs/ # Download/runtime helper scripts and assets +|-- notebooks/ # Exploratory notebooks by topic +|-- documentation/ # Sphinx docs source +|-- pyproject.toml # Canonical packaging/dependency metadata +|-- python-requirements.txt # Deprecated legacy dependency snapshot +|-- python-requirements-dev.txt # Deprecated legacy dev/test snapshot +|-- Pipfile # Deprecated legacy pipenv workflow +|-- .pylintrc # Lint configuration +|-- .travis.yml # Historical CI reference +|-- setup.py # Legacy compatibility wrapper +`-- AGENTS.md +``` + +## Environment Setup (Recommended: uv) + +Use Python 3.11 in a local `.venv`. + +```bash +cd /Users/jackeames/Downloads/LexNLP +uv python install 3.11 +uv venv --python 3.11 .venv +uv pip install --python .venv/bin/python -e ".[dev,test]" +``` + +### Deprecated setup variants + +`Pipfile`, `python-requirements.txt`, and `python-requirements-dev.txt` are deprecated. Use `uv` with `pyproject.toml` for all new local setup and CI updates. + +## Required Runtime/Test Assets + +Use the bootstrap script for deterministic setup: + +```bash +./.venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model +``` + +Optional assets: + +```bash +# Stanford +./.venv/bin/python scripts/bootstrap_assets.py --stanford + +# Tika +./.venv/bin/python scripts/bootstrap_assets.py --tika +``` + +## Stanford-Dependent Tests + +Stanford tests are gated by `LEXNLP_USE_STANFORD=true`. + +1. Install Java: +```bash +brew install openjdk +``` + +2. Ensure Java is on path for test commands: +```bash +export PATH="/opt/homebrew/opt/openjdk/bin:$PATH" +``` + +3. Download Stanford assets to `libs/stanford_nlp`: +- `stanford-postagger-full-2017-06-09` +- `stanford-ner-2017-06-09` + +Expected files: +- `libs/stanford_nlp/stanford-postagger-full-2017-06-09/stanford-postagger.jar` +- `libs/stanford_nlp/stanford-postagger-full-2017-06-09/models/english-bidirectional-distsim.tagger` +- `libs/stanford_nlp/stanford-ner-2017-06-09/stanford-ner.jar` +- `libs/stanford_nlp/stanford-ner-2017-06-09/classifiers/english.all.3class.distsim.crf.ser.gz` + +## Tika Notes + +`scripts/download_tika.sh` can fail on macOS because it assumes GNU `mkdir --parents` and `wget`. +If needed, manually download `tika-app-1.16.jar` and `tika-server-1.16.jar` into `bin/` using `curl`. + +Migration and troubleshooting details are in `MIGRATION_RUNBOOK.md`. + +## Test Integrity Policy + +- Do not add, remove, or modify `skip`, `skipif`, or `xfail` markers to bypass failures. +- Fix failing behavior or document a real external blocker; never mask regressions by changing skip behavior. +- Validation target is **100% pass** for required suites. + +## Full Validation Commands (100% pass target) + +Run in two phases: + +1. Base suite: +```bash +./.venv/bin/pytest lexnlp +``` + +2. Stanford-only suite: +```bash +PATH=/opt/homebrew/opt/openjdk/bin:$PATH \ +LEXNLP_USE_STANFORD=true \ +./.venv/bin/pytest \ + lexnlp/nlp/en/tests/test_stanford.py \ + lexnlp/extract/en/entities/tests/test_stanford_ner.py +``` + +When Stanford assets are installed and enabled, both phases must pass (0 failures) for a **100% pass** result. + +Note: a single monolithic `LEXNLP_USE_STANFORD=true` run can occasionally hang in non-Stanford modules on this machine, so prefer the two-phase approach. + +## Common Commands + +```bash +# quick dependency sanity +./.venv/bin/pip check + +# run one file +./.venv/bin/pytest lexnlp/extract/en/tests/test_dates.py + +# historical CI-style command +./.venv/bin/pytest --cov lexnlp --pylint --pylint-rcfile=.pylintrc lexnlp +``` + +## Implementation Guidelines + +- Keep changes scoped to the relevant locale/module (`extract/en`, `extract/de`, etc.). +- Add or update tests alongside behavior changes. +- Prefer existing utilities under `lexnlp/utils/` over introducing duplicates. +- When adding extraction patterns/models, include representative fixtures in `test_data/`. +- Avoid committing downloaded/generated third-party assets unless explicitly required. + +## Pull Request Checklist + +- Editable install works: `uv pip install --python .venv/bin/python -e ".[dev,test]"` +- Targeted tests for changed modules pass. +- Full base run (`pytest lexnlp`) passes. +- If Stanford assets are enabled, Stanford-only suite with `LEXNLP_USE_STANFORD=true` passes. +- No `skip`/`skipif`/`xfail` policy bypasses were introduced. +- Document any required asset downloads (NLTK, pipeline models, Stanford, Tika) in PR notes. diff --git a/MIGRATION_RUNBOOK.md b/MIGRATION_RUNBOOK.md new file mode 100644 index 0000000..fef001a --- /dev/null +++ b/MIGRATION_RUNBOOK.md @@ -0,0 +1,98 @@ +# Dependency Migration Runbook + +This runbook is the operational guide for maintaining a modern, reproducible LexNLP environment with a zero-skip testing policy. + +## 1) Toolchain Baseline + +- Python: `3.11` (default), supported range in `pyproject.toml`: `>=3.10,<3.13` +- Packaging/dependencies: `pyproject.toml` + `uv.lock` +- Installer/runner: `uv` + +Legacy files are retained for historical reproduction only: +- `Pipfile` +- `python-requirements.txt` +- `python-requirements-dev.txt` +- `python-requirements-full.txt` + +## 2) Fresh Setup + +```bash +cd /Users/jackeames/Downloads/LexNLP +uv python install 3.11 +uv venv --python 3.11 .venv +uv pip install --python .venv/bin/python -e ".[dev,test]" +``` + +## 3) Bootstrap Required Assets + +```bash +# NLTK + required model artifact +./.venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model + +# Optional: Stanford assets for Stanford-gated tests +./.venv/bin/python scripts/bootstrap_assets.py --stanford + +# Optional: Tika jars +./.venv/bin/python scripts/bootstrap_assets.py --tika +``` + +## 4) Policy Checks + +```bash +# Fail if unapproved skip/skipif/xfail markers were added +./.venv/bin/python ci/skip_audit.py +``` + +## 5) Full Validation (100% pass target) + +```bash +# Base suite +./.venv/bin/pytest lexnlp + +# Stanford-only suite (requires Stanford assets + Java) +PATH=/opt/homebrew/opt/openjdk/bin:$PATH \ +LEXNLP_USE_STANFORD=true \ +./.venv/bin/pytest \ + lexnlp/nlp/en/tests/test_stanford.py \ + lexnlp/extract/en/entities/tests/test_stanford_ner.py +``` + +Passing both commands is the required 100% result for a fully provisioned environment. + +## 6) Packaging Validation + +```bash +uv build +uv venv --python 3.11 .venv-smoke +uv pip install --python .venv-smoke/bin/python dist/*.whl +.venv-smoke/bin/python -c "import lexnlp; print(lexnlp.__version__)" +``` + +## 7) Model Upgrade Quality Gate + +Use the quality gate script before adopting a new contract-model artifact: + +```bash +./.venv/bin/python scripts/model_quality_gate.py \ + --candidate-tag pipeline/is-contract/0.2 \ + --fixture test_data/lexnlp/extract/en/contracts/tests/test_contracts/test_is_contract.csv \ + --max-f1-regression 0.0 \ + --max-accuracy-regression 0.0 +``` + +Default policy is non-regression against the baseline model (`pipeline/is-contract/0.1`). + +## 8) Failure Triage + +- `LookupError` for NLTK resources: + - Re-run `scripts/bootstrap_assets.py --nltk` +- Contract tests failing with missing model tag: + - Re-run `scripts/bootstrap_assets.py --contract-model` +- Stanford tests failing due missing jars/models: + - Re-run `scripts/bootstrap_assets.py --stanford` +- Skip-audit failure: + - Remove the marker, or add annotation: + - `# skip-audit: issue= expires=YYYY-MM-DD` + - For approved legacy skips only, update `ci/skip_audit_allowlist.txt` +- Packaging smoke failure: + - Ensure build artifacts are generated with `uv build` and install into a clean venv diff --git a/README.md b/README.md index 32b20fb..29c5496 100755 --- a/README.md +++ b/README.md @@ -42,8 +42,40 @@ evaluation license by contacting ContraxSuite Licensing at <>. ## Requirements -* Python 3.8 -* pipenv +* Python 3.11 (default; supported range is defined in `pyproject.toml`) +* `uv` + +## Quick Setup (uv + pyproject) +```bash +cd /Users/jackeames/Downloads/LexNLP +uv python install 3.11 +uv venv --python 3.11 .venv +uv pip install --python .venv/bin/python -e ".[dev,test]" +./.venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model +``` + +## Deprecated Setup Variants +`Pipfile`, `python-requirements.txt`, and `python-requirements-dev.txt` are deprecated and kept only for legacy reproduction. New development and CI updates should use `uv` with `pyproject.toml`. + +## Migration Runbook +See `MIGRATION_RUNBOOK.md` for complete migration/triage/quality-gate procedures. + +## Test Integrity and Full Validation +- Do not add/remove/modify `skip`, `skipif`, or `xfail` markers to bypass failing tests. +- Target is **100% pass**. +- If Stanford assets are enabled, 100% pass includes both base and Stanford-only suites. + +```bash +# Base suite +./.venv/bin/pytest lexnlp + +# Stanford-only suite (run when Stanford assets are installed) +PATH=/opt/homebrew/opt/openjdk/bin:$PATH \ +LEXNLP_USE_STANFORD=true \ +./.venv/bin/pytest \ + lexnlp/nlp/en/tests/test_stanford.py \ + lexnlp/extract/en/entities/tests/test_stanford_ner.py +``` ## Releases * 2.3.0: November 30, 2022 - Twenty sixth scheduled public release; [code](https://github.com/LexPredict/lexpredict-lexnlp/tree/2.3.0) diff --git a/ci/skip_audit.py b/ci/skip_audit.py new file mode 100644 index 0000000..d54df0a --- /dev/null +++ b/ci/skip_audit.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +"""Audit pytest skip markers. + +Policy: +- New pytest skip markers must include a nearby annotation: + `skip-audit: issue=... expires=YYYY-MM-DD` +- Existing markers can be grandfathered via an allowlist file. +""" + +from __future__ import annotations + +import argparse +import ast +import datetime as dt +import re +import subprocess +import sys +import warnings +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Sequence, Set, Tuple + +ANNOTATION_RE = re.compile( + r"skip-audit:\s*issue=(?P\S+)\s+expires=(?P\d{4}-\d{2}-\d{2})" +) +MARKER_NAMES = {"skip", "skipif", "xfail"} +LOOKBACK_LINES = 2 + + +@dataclass(frozen=True) +class Marker: + path: Path + line: int + col: int + kind: str + expression: str + + @property + def key(self) -> str: + return f"{self.path.as_posix()}:{self.line}:{self.kind}" + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Fail if unapproved pytest skip markers are present." + ) + script_path = Path(__file__).resolve() + default_repo_root = script_path.parent.parent + parser.add_argument( + "--repo-root", + default=str(default_repo_root), + help="Repository root path (default: %(default)s)", + ) + parser.add_argument( + "--allowlist", + default="ci/skip_audit_allowlist.txt", + help="Allowlist path (relative to repo root unless absolute).", + ) + return parser.parse_args(argv) + + +def marker_kind(node: ast.AST) -> Optional[str]: + if not isinstance(node, ast.Attribute): + return None + if node.attr not in MARKER_NAMES: + return None + mark_parent = node.value + if ( + isinstance(mark_parent, ast.Attribute) + and mark_parent.attr == "mark" + and isinstance(mark_parent.value, ast.Name) + and mark_parent.value.id == "pytest" + ): + return node.attr + return None + + +def list_python_files(repo_root: Path) -> List[Path]: + try: + result = subprocess.run( + ["git", "ls-files", "*.py"], + cwd=repo_root, + check=True, + capture_output=True, + text=True, + ) + except (FileNotFoundError, subprocess.CalledProcessError): + return sorted(path for path in repo_root.rglob("*.py") if ".git" not in path.parts) + + files = [] + for line in result.stdout.splitlines(): + relative_path = line.strip() + if not relative_path: + continue + files.append(repo_root / relative_path) + return sorted(files) + + +def collect_markers(repo_root: Path) -> Tuple[List[Marker], List[str]]: + markers: List[Marker] = [] + parse_errors: List[str] = [] + + for file_path in list_python_files(repo_root): + relative_path = file_path.relative_to(repo_root) + try: + source = file_path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + parse_errors.append( + f"{relative_path.as_posix()}: failed to decode as UTF-8 ({exc})" + ) + continue + + try: + with warnings.catch_warnings(): + # Older files can emit parser-level invalid escape warnings; + # they are unrelated to skip marker policy and should not fail the audit. + warnings.simplefilter("ignore", SyntaxWarning) + tree = ast.parse(source, filename=str(relative_path)) + except SyntaxError as exc: + parse_errors.append( + f"{relative_path.as_posix()}:{exc.lineno}: syntax error while parsing ({exc.msg})" + ) + continue + + parents: Dict[int, ast.AST] = {} + for parent in ast.walk(tree): + for child in ast.iter_child_nodes(parent): + parents[id(child)] = parent + + seen: Set[Tuple[int, int, str]] = set() + for node in ast.walk(tree): + kind: Optional[str] = None + if isinstance(node, ast.Call): + kind = marker_kind(node.func) + elif isinstance(node, ast.Attribute): + kind = marker_kind(node) + parent = parents.get(id(node)) + if isinstance(parent, ast.Call) and parent.func is node: + kind = None + + if kind is None: + continue + + key = (node.lineno, node.col_offset, kind) + if key in seen: + continue + seen.add(key) + + expression = ast.get_source_segment(source, node) or kind + markers.append( + Marker( + path=relative_path, + line=node.lineno, + col=node.col_offset, + kind=kind, + expression=" ".join(expression.split()), + ) + ) + + markers.sort(key=lambda marker: (marker.path.as_posix(), marker.line, marker.col)) + return markers, parse_errors + + +def load_allowlist(allowlist_path: Path) -> Set[str]: + if not allowlist_path.exists(): + return set() + entries: Set[str] = set() + for raw_line in allowlist_path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + entries.add(line) + return entries + + +def find_annotation(lines: Sequence[str], marker_line: int) -> Optional[re.Match[str]]: + start = max(1, marker_line - LOOKBACK_LINES) + for line_number in range(marker_line, start - 1, -1): + line = lines[line_number - 1] + match = ANNOTATION_RE.search(line) + if match: + return match + return None + + +def main(argv: Sequence[str]) -> int: + args = parse_args(argv) + repo_root = Path(args.repo_root).resolve() + allowlist_path = Path(args.allowlist) + if not allowlist_path.is_absolute(): + allowlist_path = (repo_root / allowlist_path).resolve() + + allowlist = load_allowlist(allowlist_path) + markers, parse_errors = collect_markers(repo_root) + + if parse_errors: + print("skip-audit: parse errors detected", file=sys.stderr) + for parse_error in parse_errors: + print(f" - {parse_error}", file=sys.stderr) + return 1 + + files_cache: Dict[Path, List[str]] = {} + today = dt.date.today() + violations: List[str] = [] + allowlisted_count = 0 + + for marker in markers: + if marker.key in allowlist: + allowlisted_count += 1 + continue + + lines = files_cache.setdefault( + marker.path, (repo_root / marker.path).read_text(encoding="utf-8").splitlines() + ) + annotation_match = find_annotation(lines, marker.line) + display_id = f"{marker.path.as_posix()}:{marker.line}:{marker.kind}" + if annotation_match is None: + violations.append( + f"{display_id} missing annotation `skip-audit: issue=... expires=YYYY-MM-DD`" + ) + continue + + expires_raw = annotation_match.group("expires") + try: + expires_date = dt.date.fromisoformat(expires_raw) + except ValueError: + violations.append(f"{display_id} has invalid expires date: {expires_raw}") + continue + + if expires_date < today: + violations.append( + f"{display_id} has expired annotation (expires={expires_raw}, today={today.isoformat()})" + ) + + if violations: + print("skip-audit: policy violations found", file=sys.stderr) + for violation in violations: + print(f" - {violation}", file=sys.stderr) + print( + ( + "skip-audit: either add a valid annotation near each marker or update " + f"{allowlist_path.relative_to(repo_root).as_posix()} for approved legacy markers." + ), + file=sys.stderr, + ) + return 1 + + print( + "skip-audit: OK " + f"(markers={len(markers)}, allowlisted={allowlisted_count}, " + f"annotated_new={len(markers) - allowlisted_count})" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/ci/skip_audit_allowlist.txt b/ci/skip_audit_allowlist.txt new file mode 100644 index 0000000..b199435 --- /dev/null +++ b/ci/skip_audit_allowlist.txt @@ -0,0 +1,13 @@ +# Grandfathered Stanford skip markers. +# Format: path:line:marker +lexnlp/extract/en/entities/tests/test_stanford_ner.py:27:skipif +lexnlp/extract/en/entities/tests/test_stanford_ner.py:35:skipif +lexnlp/extract/en/entities/tests/test_stanford_ner.py:43:skipif +lexnlp/nlp/en/tests/test_stanford.py:37:skipif +lexnlp/nlp/en/tests/test_stanford.py:44:skipif +lexnlp/nlp/en/tests/test_stanford.py:51:skipif +lexnlp/nlp/en/tests/test_stanford.py:58:skipif +lexnlp/nlp/en/tests/test_stanford.py:65:skipif +lexnlp/nlp/en/tests/test_stanford.py:72:skipif +lexnlp/nlp/en/tests/test_stanford.py:79:skipif +lexnlp/nlp/en/tests/test_stanford.py:86:skipif diff --git a/lexnlp/extract/es/regulations.py b/lexnlp/extract/es/regulations.py index e6efa3a..e0e223c 100644 --- a/lexnlp/extract/es/regulations.py +++ b/lexnlp/extract/es/regulations.py @@ -52,7 +52,20 @@ def load_trigger_words(self) -> None: dtypes = {'trigger': str, 'position': str} if not self.regulations_dataframe: path = os.path.join(lexnlp_base_path, 'lexnlp/config/es/es_regulations.csv') - self.regulations_dataframe = read_csv(path, encoding='utf-8', error_bad_lines=False, converters=dtypes) + try: + # pandas >= 1.3 + self.regulations_dataframe = read_csv( + path, + encoding='utf-8', + on_bad_lines='skip', + converters=dtypes) + except TypeError: + # pandas < 1.3 + self.regulations_dataframe = read_csv( + path, + encoding='utf-8', + error_bad_lines=False, + converters=dtypes) subset = self.regulations_dataframe[['trigger', 'position']] tuples = [tuple(x) for x in subset.values] self.start_triggers = [t[0] for t in tuples if t[1] == 'start'] diff --git a/lexnlp/ml/predictor.py b/lexnlp/ml/predictor.py index 066214a..4077818 100644 --- a/lexnlp/ml/predictor.py +++ b/lexnlp/ml/predictor.py @@ -84,12 +84,32 @@ def __init__(self, pipeline: Optional[Pipeline] = None) -> None: f'does not follow the `ScikitLearnHasPredictProba` protocol.' ) + self._patch_legacy_estimator_attributes() + # Fix AttributeError: 'MinMaxScaler' object has no attribute 'clip' for _, name, transform in self.pipeline._iter(with_final=False): transform.clip = hasattr(transform, 'clip') and transform.clip self._sanity_check() + def _patch_legacy_estimator_attributes(self) -> None: + """ + Patch known attribute-renames for old serialized Scikit-Learn estimators. + + LexNLP bundles model artifacts trained on older Scikit-Learn versions. + Newer runtimes may rename fitted attributes and break inference unless + we provide compatible aliases. + """ + estimator = self.pipeline._final_estimator + + # sklearn.naive_bayes.GaussianNB previously persisted `sigma_` and now + # expects `var_`/`variance_` in prediction paths. + if hasattr(estimator, "sigma_"): + if not hasattr(estimator, "var_"): + estimator.var_ = estimator.sigma_ + if not hasattr(estimator, "variance_"): + estimator.variance_ = estimator.var_ + @abstractmethod def _sanity_check(self) -> None: """ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1e70cdb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,98 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "lexnlp" +version = "2.3.0" +description = "LexPredict LexNLP" +readme = { file = "README.rst", content-type = "text/x-rst" } +requires-python = ">=3.10,<3.13" +license = "AGPL-3.0-or-later" +authors = [ + { name = "ContraxSuite, LLC", email = "support@contraxsuite.com" } +] +keywords = [ + "legal", + "contract", + "document analytics", + "nlp", + "ml", + "machine learning", + "natural language" +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Information Technology", + "Intended Audience :: Legal Industry", + "Intended Audience :: Developers", + "Natural Language :: English", + "Topic :: Office/Business", + "Topic :: Text Processing :: Linguistic", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12" +] +dependencies = [ + "beautifulsoup4>=4.11.1,<5", + "cloudpickle>=2.2.0,<4", + "dateparser==1.1.3", + "elasticsearch>=8.5.0,<9", + "gensim>=4.3.2,<5", + "importlib-metadata>=5.0.0; python_version < '3.10'", + "joblib>=1.2.0,<2", + "lxml>=4.9.1,<6", + "nltk>=3.8.1,<3.9", + "num2words>=0.5.12,<1", + "numpy>=1.24.0,<2", + "pandas>=1.5.3,<2", + "psutil>=5.9.4,<7", + "pycountry>=22.3.5,<25", + "python-dateutil>=2.8.2,<3", + "regex==2022.3.2", + "reporters-db>=3.2.32,<4", + "requests>=2.28.1,<3", + "scikit-learn>=1.2.2,<1.3", + "scipy>=1.10.0,<1.11", + "tqdm>=4.64.1,<5", + "Unidecode>=1.3.6,<2", + "us>=2.0.2,<3", + "zahlwort2num>=0.4.2,<1" +] + +[project.optional-dependencies] +dev = [ + "build>=1.2.2", + "memory-profiler>=0.61.0", + "nose>=1.3.7,<2", + "pylint>=3.3.0", + "pytest>=8.3.0", + "pytest-cov>=5.0.0", + "pytest-xdist>=3.6.0", + "sphinx>=7.4.0", + "twine>=5.1.1" +] +test = [ + "nose>=1.3.7,<2", + "pytest>=8.3.0", + "pytest-cov>=5.0.0", + "pytest-xdist>=3.6.0" +] +stanford = [] +tika = ["tika>=2.6.0"] + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +exclude = ["*tests*"] + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.pytest.ini_options] +markers = [ + "serial: sequential-only tests" +] diff --git a/scripts/bootstrap_assets.py b/scripts/bootstrap_assets.py new file mode 100755 index 0000000..78e8225 --- /dev/null +++ b/scripts/bootstrap_assets.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +"""Deterministic cross-platform bootstrap utility for LexNLP assets.""" + +from __future__ import annotations + +import argparse +import logging +import sys +import zipfile +from pathlib import Path +from typing import Iterable, List, Sequence, Tuple +from urllib.request import Request, urlopen + +LOGGER = logging.getLogger("lexnlp.bootstrap") + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_STANFORD_DIR = REPO_ROOT / "libs" / "stanford_nlp" +DEFAULT_TIKA_DIR = REPO_ROOT / "bin" + +NLTK_RESOURCES = ( + "punkt", + "wordnet", + "omw-1.4", + "averaged_perceptron_tagger", + "maxent_ne_chunker", + "words", +) +OPTIONAL_NLTK_RESOURCES = ("punkt_tab",) + +CONTRACT_MODEL_TAG = "pipeline/is-contract/0.1" + +STANFORD_DOWNLOADS: Tuple[Tuple[str, str, Tuple[str, ...]], ...] = ( + ( + "stanford-postagger-full-2017-06-09.zip", + "https://nlp.stanford.edu/software/stanford-postagger-full-2017-06-09.zip", + ( + "stanford-postagger-full-2017-06-09/stanford-postagger.jar", + "stanford-postagger-full-2017-06-09/models/english-bidirectional-distsim.tagger", + ), + ), + ( + "stanford-ner-2017-06-09.zip", + "https://nlp.stanford.edu/software/stanford-ner-2017-06-09.zip", + ( + "stanford-ner-2017-06-09/stanford-ner.jar", + "stanford-ner-2017-06-09/classifiers/english.all.3class.distsim.crf.ser.gz", + ), + ), +) + +TIKA_DOWNLOADS: Tuple[Tuple[str, str], ...] = ( + ( + "tika-app-1.16.jar", + "https://archive.apache.org/dist/tika/tika-app-1.16.jar", + ), + ( + "tika-server-1.16.jar", + "https://archive.apache.org/dist/tika/tika-server-1.16.jar", + ), +) + + +class BootstrapError(Exception): + """Raised when one or more bootstrap tasks fail.""" + + +def configure_logging(verbose: bool) -> None: + level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig(level=level, format="[bootstrap][%(levelname)s] %(message)s") + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Bootstrap LexNLP runtime/test assets in a deterministic way.", + ) + parser.add_argument("--nltk", action="store_true", help="Download required NLTK resources.") + parser.add_argument( + "--contract-model", + action="store_true", + help="Download LexNLP contract model release pipeline/is-contract/0.1.", + ) + parser.add_argument( + "--stanford", + action="store_true", + help="Download Stanford POS tagger and NER ZIP artifacts.", + ) + parser.add_argument( + "--tika", + action="store_true", + help="Download Apache Tika 1.16 app/server jars.", + ) + parser.add_argument("--all", action="store_true", help="Run all bootstrap tasks.") + parser.add_argument( + "--stanford-dir", + default=str(DEFAULT_STANFORD_DIR), + help="Destination directory for Stanford ZIPs (default: libs/stanford_nlp).", + ) + parser.add_argument( + "--tika-dir", + default=str(DEFAULT_TIKA_DIR), + help="Destination directory for Tika jars (default: bin).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print planned actions without network/file writes.", + ) + parser.add_argument( + "--force", + action="store_true", + help="Re-download files even if destination files already exist.", + ) + parser.add_argument( + "--timeout", + type=int, + default=60, + help="Network timeout in seconds for each request (default: 60).", + ) + parser.add_argument("--verbose", action="store_true", help="Enable debug logging.") + + args = parser.parse_args(argv) + + if not any((args.nltk, args.contract_model, args.stanford, args.tika, args.all)): + parser.error("Select at least one task: --nltk, --contract-model, --stanford, --tika, or --all") + + if args.timeout <= 0: + parser.error("--timeout must be a positive integer") + + return args + + +def ensure_directory(path: Path, dry_run: bool) -> None: + if dry_run: + LOGGER.info("DRY RUN: would create directory %s", path) + return + path.mkdir(parents=True, exist_ok=True) + + +def download_file( + url: str, + destination: Path, + *, + force: bool, + dry_run: bool, + timeout: int, +) -> None: + if destination.exists() and not force: + LOGGER.info("Skipping existing file: %s", destination) + return + + ensure_directory(destination.parent, dry_run=dry_run) + + if dry_run: + LOGGER.info("DRY RUN: would download %s -> %s", url, destination) + return + + tmp_destination = destination.with_name(destination.name + ".part") + if tmp_destination.exists(): + tmp_destination.unlink() + + request = Request(url, headers={"User-Agent": "lexnlp-bootstrap/1.0"}) + LOGGER.info("Downloading %s", url) + + try: + with urlopen(request, timeout=timeout) as response, tmp_destination.open("wb") as output_file: + while True: + chunk = response.read(64 * 1024) + if not chunk: + break + output_file.write(chunk) + tmp_destination.replace(destination) + except Exception: + if tmp_destination.exists(): + tmp_destination.unlink() + raise + + LOGGER.info("Saved %s", destination) + + +def download_many( + downloads: Iterable[Tuple[str, str]], + destination_dir: Path, + *, + force: bool, + dry_run: bool, + timeout: int, +) -> None: + for filename, url in downloads: + destination = destination_dir / filename + download_file(url, destination, force=force, dry_run=dry_run, timeout=timeout) + + +def extract_zip(archive_path: Path, destination_dir: Path, *, dry_run: bool) -> None: + if dry_run: + LOGGER.info("DRY RUN: would extract %s -> %s", archive_path, destination_dir) + return + LOGGER.info("Extracting %s", archive_path) + with zipfile.ZipFile(archive_path) as archive: + archive.extractall(destination_dir) + + +def bootstrap_stanford_assets( + destination_dir: Path, + *, + force: bool, + dry_run: bool, + timeout: int, +) -> None: + ensure_directory(destination_dir, dry_run=dry_run) + for filename, url, required_files in STANFORD_DOWNLOADS: + required_paths = [destination_dir / rel_path for rel_path in required_files] + if all(path.exists() for path in required_paths) and not force: + LOGGER.info("Skipping Stanford asset already present: %s", filename) + continue + + archive_path = destination_dir / filename + download_file(url, archive_path, force=force, dry_run=dry_run, timeout=timeout) + extract_zip(archive_path, destination_dir, dry_run=dry_run) + + if dry_run: + return + + missing = [] + for _, _, required_files in STANFORD_DOWNLOADS: + for required_file in required_files: + candidate = destination_dir / required_file + if not candidate.exists(): + missing.append(candidate) + if missing: + missing_display = ", ".join(str(path) for path in missing) + raise RuntimeError(f"Missing required Stanford files after download: {missing_display}") + + +def bootstrap_nltk(*, dry_run: bool) -> None: + if dry_run: + for resource in NLTK_RESOURCES: + LOGGER.info("DRY RUN: would download NLTK resource %s", resource) + return + + try: + import nltk + except ImportError as error: + raise RuntimeError("nltk is required for --nltk. Install dependencies first.") from error + + for resource in NLTK_RESOURCES: + LOGGER.info("Downloading NLTK resource: %s", resource) + nltk.download(resource, quiet=True, raise_on_error=True) + + for resource in OPTIONAL_NLTK_RESOURCES: + LOGGER.info("Attempting optional NLTK resource: %s", resource) + try: + nltk.download(resource, quiet=True, raise_on_error=True) + except Exception: + LOGGER.warning("Optional NLTK resource unavailable: %s", resource) + + +def bootstrap_contract_model(*, dry_run: bool) -> None: + if dry_run: + LOGGER.info("DRY RUN: would download LexNLP model tag %s", CONTRACT_MODEL_TAG) + return + + try: + from lexnlp.ml.catalog.download import download_github_release + except ImportError as error: + raise RuntimeError( + "Unable to import LexNLP catalog downloader. Ensure dependencies and editable install are in place." + ) from error + + LOGGER.info("Downloading LexNLP contract model: %s", CONTRACT_MODEL_TAG) + download_github_release(CONTRACT_MODEL_TAG, prompt_user=False) + + +def run_selected_tasks(args: argparse.Namespace) -> None: + run_nltk = args.all or args.nltk + run_contract_model = args.all or args.contract_model + run_stanford = args.all or args.stanford + run_tika = args.all or args.tika + + tasks: List[Tuple[str, object]] = [] + if run_nltk: + tasks.append(("nltk", lambda: bootstrap_nltk(dry_run=args.dry_run))) + if run_contract_model: + tasks.append(("contract-model", lambda: bootstrap_contract_model(dry_run=args.dry_run))) + if run_stanford: + stanford_dir = Path(args.stanford_dir).expanduser().resolve() + tasks.append( + ( + "stanford", + lambda: bootstrap_stanford_assets( + stanford_dir, + force=args.force, + dry_run=args.dry_run, + timeout=args.timeout, + ), + ) + ) + if run_tika: + tika_dir = Path(args.tika_dir).expanduser().resolve() + tasks.append( + ( + "tika", + lambda: download_many( + TIKA_DOWNLOADS, + tika_dir, + force=args.force, + dry_run=args.dry_run, + timeout=args.timeout, + ), + ) + ) + + failures: List[str] = [] + for name, task in tasks: + LOGGER.info("Starting task: %s", name) + try: + task() + LOGGER.info("Finished task: %s", name) + except Exception: + LOGGER.exception("Task failed: %s", name) + failures.append(name) + + if failures: + raise BootstrapError("Failed tasks: {}".format(", ".join(failures))) + + +def main(argv: Sequence[str]) -> int: + args = parse_args(argv) + configure_logging(args.verbose) + + LOGGER.debug("Repository root: %s", REPO_ROOT) + if args.dry_run: + LOGGER.info("Dry-run mode enabled; no downloads or filesystem writes will occur.") + + try: + run_selected_tasks(args) + except BootstrapError as error: + LOGGER.error(str(error)) + return 1 + + LOGGER.info("Bootstrap tasks completed successfully.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/scripts/model_quality_gate.py b/scripts/model_quality_gate.py new file mode 100644 index 0000000..84a7fa1 --- /dev/null +++ b/scripts/model_quality_gate.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Quality gate for contract-classifier model upgrades.""" + +from __future__ import annotations + +import argparse +import csv +import json +import sys +from pathlib import Path +from typing import Dict, List, Sequence, Tuple + + +DEFAULT_FIXTURE = Path( + "test_data/lexnlp/extract/en/contracts/tests/test_contracts/test_is_contract.csv" +) + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Compare baseline and candidate contract models on a labeled fixture.", + ) + parser.add_argument( + "--baseline-tag", + default="pipeline/is-contract/0.1", + help="Catalog tag used as baseline model.", + ) + parser.add_argument( + "--candidate-tag", + required=True, + help="Catalog tag used as candidate model.", + ) + parser.add_argument( + "--fixture", + type=Path, + default=DEFAULT_FIXTURE, + help=f"CSV fixture path (default: {DEFAULT_FIXTURE})", + ) + parser.add_argument( + "--min-probability", + type=float, + default=0.3, + help="Classification threshold used by is_contract.", + ) + parser.add_argument( + "--max-accuracy-regression", + type=float, + default=0.0, + help="Maximum allowed candidate accuracy drop vs baseline.", + ) + parser.add_argument( + "--max-f1-regression", + type=float, + default=0.0, + help="Maximum allowed candidate F1 drop vs baseline.", + ) + parser.add_argument( + "--min-candidate-accuracy", + type=float, + default=0.0, + help="Absolute minimum candidate accuracy.", + ) + parser.add_argument( + "--output-json", + type=Path, + help="Optional path to write JSON results.", + ) + return parser.parse_args(argv) + + +def load_fixture(path: Path) -> Tuple[List[str], List[bool]]: + if not path.exists(): + raise FileNotFoundError(f"Fixture file not found: {path}") + + texts: List[str] = [] + labels: List[bool] = [] + + with path.open("r", encoding="utf-8", newline="") as fixture_file: + reader = csv.DictReader(fixture_file) + for row in reader: + text = row["Text"] + label_raw = row["Is_Contract"].strip().lower() + if label_raw not in {"true", "false"}: + raise ValueError(f"Unexpected label value: {row['Is_Contract']}") + texts.append(text) + labels.append(label_raw == "true") + + if not texts: + raise ValueError(f"Fixture file contains no rows: {path}") + return texts, labels + + +def ensure_tag_downloaded(tag: str) -> Path: + from lexnlp.ml.catalog import get_path_from_catalog + from lexnlp.ml.catalog.download import download_github_release + + try: + return get_path_from_catalog(tag) + except FileNotFoundError: + download_github_release(tag, prompt_user=False) + return get_path_from_catalog(tag) + + +def load_pipeline_for_tag(tag: str): + from cloudpickle import load + + model_path = ensure_tag_downloaded(tag) + with model_path.open("rb") as model_file: + return load(model_file) + + +def score_pipeline(pipeline, texts: List[str], labels: List[bool], min_probability: float) -> Dict[str, float]: + from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score + from lexnlp.extract.en.contracts.predictors import ProbabilityPredictorIsContract + + predictor = ProbabilityPredictorIsContract(pipeline=pipeline) + predictions = [ + bool(predictor.is_contract(text=text, min_probability=min_probability)) + for text in texts + ] + return { + "accuracy": float(accuracy_score(labels, predictions)), + "f1": float(f1_score(labels, predictions)), + "precision": float(precision_score(labels, predictions, zero_division=0)), + "recall": float(recall_score(labels, predictions, zero_division=0)), + } + + +def main(argv: Sequence[str]) -> int: + args = parse_args(argv) + texts, labels = load_fixture(args.fixture) + + baseline_metrics = score_pipeline( + load_pipeline_for_tag(args.baseline_tag), + texts, + labels, + args.min_probability, + ) + candidate_metrics = score_pipeline( + load_pipeline_for_tag(args.candidate_tag), + texts, + labels, + args.min_probability, + ) + + result = { + "baseline_tag": args.baseline_tag, + "candidate_tag": args.candidate_tag, + "fixture": str(args.fixture), + "min_probability": args.min_probability, + "baseline": baseline_metrics, + "candidate": candidate_metrics, + } + + print(json.dumps(result, indent=2, sort_keys=True)) + + violations: List[str] = [] + accuracy_drop = baseline_metrics["accuracy"] - candidate_metrics["accuracy"] + f1_drop = baseline_metrics["f1"] - candidate_metrics["f1"] + + if accuracy_drop > args.max_accuracy_regression: + violations.append( + "accuracy regression exceeds threshold " + f"({accuracy_drop:.6f} > {args.max_accuracy_regression:.6f})" + ) + if f1_drop > args.max_f1_regression: + violations.append( + f"f1 regression exceeds threshold ({f1_drop:.6f} > {args.max_f1_regression:.6f})" + ) + if candidate_metrics["accuracy"] < args.min_candidate_accuracy: + violations.append( + "candidate accuracy below minimum " + f"({candidate_metrics['accuracy']:.6f} < {args.min_candidate_accuracy:.6f})" + ) + + if args.output_json: + args.output_json.parent.mkdir(parents=True, exist_ok=True) + args.output_json.write_text(json.dumps(result, indent=2, sort_keys=True), encoding="utf-8") + + if violations: + for violation in violations: + print(f"QUALITY GATE VIOLATION: {violation}") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index f3a0032..26e08e4 --- a/setup.py +++ b/setup.py @@ -1,153 +1,5 @@ -"""A setuptools based setup module. +from setuptools import setup -See: -https://packaging.python.org/en/latest/distributing.html -https://github.com/pypa/sampleproject -""" -# To use a consistent encoding -from os import path - -# Always prefer setuptools over distutils -from setuptools import setup, find_packages - -here = path.abspath(path.dirname(__file__)) - -# Get the long description from the README file -try: - with open(path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() -except Exception as e: - long_description = "LexPredict LexNLP: A swiss-army knife library built for working with real, unstructured legal text." - -setup( - name='lexnlp', - - # Versions should comply with PEP440. For a discussion on single-sourcing - # the version across setup.py and the project code, see - # https://packaging.python.org/en/latest/single_source_version.html - version='2.3.0', - - description='LexPredict LexNLP', - long_description=long_description, - - # The project's main homepage. - url='https://contraxsuite.com', - - # Author details - author='ContraxSuite, LLC', - author_email='support@contraxsuite.com', - - # Choose your license - license='AGPL', - - # version ranges for supported Python distributions - python_requires='~=3.6', - - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - 'Development Status :: 4 - Beta', - - # Indicate who your project is intended for - 'Intended Audience :: Information Technology', - 'Intended Audience :: Legal Industry', - 'Intended Audience :: Developers', - - # Pick your license as you wish (should match "license" above) - # 'License :: OSI Approved :: MIT License', - 'License :: OSI Approved :: GNU Affero General Public License v3', - 'License :: Other/Proprietary License', - - # Specify the Python versions you support here. In particular, ensure - # that you indicate whether you support Python 2, Python 3 or both. - 'Programming Language :: Python :: 3.6', - - # Topics - 'Natural Language :: English', - 'Topic :: Office/Business', - 'Topic :: Text Processing :: Linguistic', - - ], - - # What does your project relate to? - keywords='legal contract document analytics nlp ml machine learning natural language', - - # You can just specify the packages manually here if your project is - # simple. Or you can use find_packages(). - packages=find_packages(exclude=['*tests*']), - - # Alternatively, if you want to distribute just a my_module.py, uncomment - # this: - # py_modules=['lexnlp'], - - # List run-time dependencies here. These will be installed by pip when - # your project is installed. For an analysis of "install_requires" vs pip's - # requirements files see: - # https://packaging.python.org/en/latest/requirements.html - install_requires=[ - 'beautifulsoup4==4.11.1', - 'cloudpickle==2.2.0', - 'dateparser==1.1.3', - 'elasticsearch==8.5.0', - 'gensim==4.1.2', - 'importlib-metadata==5.0.0', - 'joblib==1.2.0', - 'lxml==4.9.1', - 'nltk==3.7', - 'num2words==0.5.12', - 'numpy==1.23.4', - 'pandas==1.5.1', - 'psutil==5.9.4', - 'pycountry==22.3.5', - 'python-dateutil==2.8.2', - 'regex==2022.3.2', - 'reporters-db==3.2.32', - 'requests==2.28.1', - 'scikit-learn==0.24', - 'scipy==1.9.3', - 'tqdm==4.64.1', - 'Unidecode==1.3.6', - 'us==2.0.2', - 'zahlwort2num==0.4.2' - ], - - # Install any data files from packages. - # The data files must be specified via the distutils’ MANIFEST.in file. - include_package_data=True, - - # List additional groups of dependencies here (e.g. development - # dependencies). You can install these using the following syntax, - # for example: - # $ pip install -e .[dev,test] - extras_require={ - 'dev': ['pytest>=2.8.5', 'mock', 'pytz>=2015.7', 'nose', 'memory-profiler', 'psutil', 'matplotlib', - 'Sphinx>=5.3.0'], - 'test': ['pytest>=2.8.5', 'mock', 'pytz>=2015.7', 'nose', 'memory-profiler', 'psutil', 'Sphinx>=5.3.0'], - }, - - # If there are data files included in your packages that need to be - # installed, specify them here. If using Python 2.6 or less, then these - # have to be included in MANIFEST.in as well. - # package_data={ - # 'sample': ['package_data.dat'], - # }, - - # Although 'package_data' is the preferred approach, in some case you may - # need to place data files outside of your packages. See: - # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa - # In this case, 'data_file' will be installed into '/my_data' - # data_files=[('my_data', ['data/data_file'])], - - # To provide executable scripts, use entry points in preference to the - # "scripts" keyword. Entry points provide cross-platform support and allow - # pip to create the appropriate form of executable for the target platform. - # entry_points={ - # 'console_scripts': [ - # 'sample=sample:main', - # ], - # }, -) +if __name__ == "__main__": + setup() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d1cedc0 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1696 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.13" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "build" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", size = 24141, upload-time = "2026-01-08T16:41:46.453Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, +] + +[[package]] +name = "dateparser" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/48/eb80ecda9fa67bae5d423dc8fd7d0fc3b6995ef698812bcf76d648ce53c2/dateparser-1.1.3.tar.gz", hash = "sha256:ae7a7de30f26983d09fff802c1f9d35d54e1c11d7ab52ae904a1f3fc037ecba5", size = 293781, upload-time = "2022-11-03T10:53:40.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/ef/f90aada30357af0c8c502e300322816a799d9d8ef109b5f10542b4aaa8ec/dateparser-1.1.3-py2.py3-none-any.whl", hash = "sha256:711f7eef6d431225bec56c00e386af3f6a47083276253375bdae1ae6c8d23d4a", size = 292670, upload-time = "2022-11-03T10:53:38.21Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "elastic-transport" +version = "8.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/54/d498a766ac8fa475f931da85a154666cc81a70f8eb4a780bc8e4e934e9ac/elastic_transport-8.17.1.tar.gz", hash = "sha256:5edef32ac864dca8e2f0a613ef63491ee8d6b8cfb52881fa7313ba9290cac6d2", size = 73425, upload-time = "2025-03-13T07:28:30.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/cd/b71d5bc74cde7fc6fd9b2ff9389890f45d9762cbbbf81dc5e51fd7588c4a/elastic_transport-8.17.1-py3-none-any.whl", hash = "sha256:192718f498f1d10c5e9aa8b9cf32aed405e469a7f0e9d6a8923431dbb2c59fb8", size = 64969, upload-time = "2025-03-13T07:28:29.031Z" }, +] + +[[package]] +name = "elasticsearch" +version = "8.19.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "elastic-transport" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/79/365e306017a9fcfbbefab1a3b588d2404bea8806b36766ff0f886509a20e/elasticsearch-8.19.3.tar.gz", hash = "sha256:e84dd618a220cac25b962790085045dd27ac72e01c0a5d81bd29a2d47a71f03f", size = 800298, upload-time = "2025-12-23T12:56:00.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/0f/ac126833c385b06166d41c486e4911f58ad7791fd1a53dd6e0b8d16ff214/elasticsearch-8.19.3-py3-none-any.whl", hash = "sha256:fe1db2555811192e8a1be78b01234d0a49d32b185ea7eeeb6f059331dee32838", size = 952820, upload-time = "2025-12-23T12:55:56.796Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "gensim" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, + { name = "smart-open" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/80/fe9d2e1ace968041814dbcfce4e8499a643a36c41267fa4b6c4f54cce420/gensim-4.4.0.tar.gz", hash = "sha256:a3f5b626da5518e79a479140361c663089fe7998df8ba52d56e1ded71ac5bdf5", size = 23260095, upload-time = "2025-10-18T02:06:45.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/88/1e7c7abf79cf88faca3d713fbb7068f58c9f44c77a3e72031cb3e40e43c3/gensim-4.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e29a2109819fdf5ff59bef670c8c22c1690d52239fe172b43e408908871de5f6", size = 24455330, upload-time = "2025-10-18T01:47:12.563Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2f/46a661db005730de7455090cb980b70147f04a3d162b49171582987d634e/gensim-4.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c4d8f2a5e69bc246931dfd8e03d0ce3f3bcf82adbbdbcf20dfc35c43b8e1035", size = 24444343, upload-time = "2025-10-18T01:47:57.596Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d8/ea8f98e198d8682c0d82cba04303d26f646ef2592a558739d812bfe02a3f/gensim-4.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0977e5e5df03f829f322662e37ac973b93272c526f1432f865d214c0b573f98", size = 27591522, upload-time = "2025-10-18T01:48:48.543Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6e/9b835483f776ad0ab6fd1197441000c4005b0a3219d456b25296966f0107/gensim-4.4.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d56613fcb77d4068c1be845843508dcd9d384ede34700a61bbeac32b947d1fc3", size = 27631604, upload-time = "2025-10-18T01:49:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/e483909cfbfa8cc4bfd30aa9fb5170c04316cc22f23c9906529f08fb9095/gensim-4.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:724b93c9b6e92cd15837048c71b7fdd38059276c85dd1f9c0375576f0aea153f", size = 24395966, upload-time = "2025-10-18T01:50:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/81b6c74b32700ee63f6720a60ca0c89ab59b12933257b47572c8af017658/gensim-4.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7590e7313848ca8f3ff064898bcd6ecf6ec71c752cf4d3ec83f7ac992bc7c088", size = 24463159, upload-time = "2025-10-18T01:51:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/38/7c/18d40f341276a7461962512ca1fb716d5982db57615dfa272f651ecb96d7/gensim-4.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a027238b5eb544a17afe73ec227d6a7e0c6b4e2108b1131c0b8f291a0e0e2e", size = 24453170, upload-time = "2025-10-18T01:51:58.42Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/6bd6919d31bdd473472ce1c18c24fcab5869b8b15166a424d11ce33a5eab/gensim-4.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e110e2d3533f5b35239850a96cb2016a586ecd85671d655079b3048332b7169", size = 27760793, upload-time = "2025-10-18T01:52:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fa/85531b39c1beb5a4203929ba83d94d886cec40d0fb0bef8ca05fd1cc7a38/gensim-4.4.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91a7fa5e814e7b1bad4b2dffa8d62c1e55410d5cbdf930714c1997ffb4404db8", size = 27809988, upload-time = "2025-10-18T01:53:36.978Z" }, + { url = "https://files.pythonhosted.org/packages/10/c3/7e22d6f7d88c4ea6a3a84481f00538252659d285713c3b7e2e1537b0e7e1/gensim-4.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:5e2c1d584d1c7d16b2a0fe7d2f6f59a451422df7b5edb7e3ca46c8e462782127", size = 24396172, upload-time = "2025-10-18T01:54:25.711Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/d5285865ca54b93d41ccd8683c2d79952434957c76b411283c7a6c66ca69/gensim-4.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0845b2fa039dbea5667fb278b5414e70f6d48fd208ef51f33e84a78444288d8d", size = 24467245, upload-time = "2025-10-18T01:55:09.924Z" }, + { url = "https://files.pythonhosted.org/packages/32/59/f0ea443cbfb3b06e1d2e060217bb91f954845f6df38cbc9c5468b6c9c638/gensim-4.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1853fc5be730f692c444a826041fef9a2fc8d74c73bb59748904b2e3221daa86", size = 24455775, upload-time = "2025-10-18T01:55:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/9b0ba15756e41ccfdd852f9c65cd2b552f240c201dc3237ad8c178642e80/gensim-4.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23a2a4260f01c8f71bae5dd0e8a01bb247a2c789480c033e0eaba100b0ad4239", size = 27771345, upload-time = "2025-10-18T01:56:41.448Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/c29701826c963b04a43d5d7b87573a74040387ab9219e65b10f377d22b5b/gensim-4.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b73ff30af6ddd0d2ddf9473b1eb44603cd79ec14c87d93b75291802b991916c", size = 27864118, upload-time = "2025-10-18T01:57:32.428Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f2/9ec6863143888bf390cdc5261f6d9e71d79bc95d98fb815679dba478d5f6/gensim-4.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3a3f9bc8d4178b01d114e1c58c5ab2333f131c7415fb3d8ec8f1ecfe4c5b544", size = 24400277, upload-time = "2025-10-18T01:58:17.629Z" }, +] + +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jellyfish" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/3f/60ac86fb43dfbf976768e80674b5538e535f6eca5aa7806cf2fdfd63550f/jellyfish-0.6.1.tar.gz", hash = "sha256:5104e45a2b804b48a46a92a5e6d6e86830fe60ae83b1da32c867402c8f4c2094", size = 132560, upload-time = "2018-04-16T14:01:25.629Z" } + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "lexnlp" +version = "2.3.0" +source = { editable = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "cloudpickle" }, + { name = "dateparser" }, + { name = "elasticsearch" }, + { name = "gensim" }, + { name = "joblib" }, + { name = "lxml" }, + { name = "nltk" }, + { name = "num2words" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "psutil" }, + { name = "pycountry" }, + { name = "python-dateutil" }, + { name = "regex" }, + { name = "reporters-db" }, + { name = "requests" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "tqdm" }, + { name = "unidecode" }, + { name = "us" }, + { name = "zahlwort2num" }, +] + +[package.optional-dependencies] +dev = [ + { name = "build" }, + { name = "memory-profiler" }, + { name = "nose" }, + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "twine" }, +] +test = [ + { name = "nose" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, +] +tika = [ + { name = "tika" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.11.1,<5" }, + { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.2" }, + { name = "cloudpickle", specifier = ">=2.2.0,<4" }, + { name = "dateparser", specifier = "==1.1.3" }, + { name = "elasticsearch", specifier = ">=8.5.0,<9" }, + { name = "gensim", specifier = ">=4.3.2,<5" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=5.0.0" }, + { name = "joblib", specifier = ">=1.2.0,<2" }, + { name = "lxml", specifier = ">=4.9.1,<6" }, + { name = "memory-profiler", marker = "extra == 'dev'", specifier = ">=0.61.0" }, + { name = "nltk", specifier = ">=3.8.1,<3.9" }, + { name = "nose", marker = "extra == 'dev'", specifier = ">=1.3.7,<2" }, + { name = "nose", marker = "extra == 'test'", specifier = ">=1.3.7,<2" }, + { name = "num2words", specifier = ">=0.5.12,<1" }, + { name = "numpy", specifier = ">=1.24.0,<2" }, + { name = "pandas", specifier = ">=1.5.3,<2" }, + { name = "psutil", specifier = ">=5.9.4,<7" }, + { name = "pycountry", specifier = ">=22.3.5,<25" }, + { name = "pylint", marker = "extra == 'dev'", specifier = ">=3.3.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0.0" }, + { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.6.0" }, + { name = "pytest-xdist", marker = "extra == 'test'", specifier = ">=3.6.0" }, + { name = "python-dateutil", specifier = ">=2.8.2,<3" }, + { name = "regex", specifier = "==2022.3.2" }, + { name = "reporters-db", specifier = ">=3.2.32,<4" }, + { name = "requests", specifier = ">=2.28.1,<3" }, + { name = "scikit-learn", specifier = ">=1.2.2,<1.3" }, + { name = "scipy", specifier = ">=1.10.0,<1.11" }, + { name = "sphinx", marker = "extra == 'dev'", specifier = ">=7.4.0" }, + { name = "tika", marker = "extra == 'tika'", specifier = ">=2.6.0" }, + { name = "tqdm", specifier = ">=4.64.1,<5" }, + { name = "twine", marker = "extra == 'dev'", specifier = ">=5.1.1" }, + { name = "unidecode", specifier = ">=1.3.6,<2" }, + { name = "us", specifier = ">=2.0.2,<3" }, + { name = "zahlwort2num", specifier = ">=0.4.2,<1" }, +] +provides-extras = ["dev", "test", "stanford", "tika"] + +[[package]] +name = "lxml" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838, upload-time = "2025-04-23T01:44:29.325Z" }, + { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827, upload-time = "2025-04-23T01:44:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098, upload-time = "2025-04-23T01:44:35.809Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261, upload-time = "2025-04-23T01:44:38.271Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621, upload-time = "2025-04-23T01:44:40.921Z" }, + { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231, upload-time = "2025-04-23T01:44:43.871Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279, upload-time = "2025-04-23T01:44:46.632Z" }, + { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405, upload-time = "2025-04-23T01:44:49.843Z" }, + { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169, upload-time = "2025-04-23T01:44:52.791Z" }, + { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691, upload-time = "2025-04-23T01:44:56.108Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503, upload-time = "2025-04-23T01:44:59.222Z" }, + { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346, upload-time = "2025-04-23T01:45:02.088Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139, upload-time = "2025-04-23T01:45:04.582Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609, upload-time = "2025-04-23T01:45:07.649Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285, upload-time = "2025-04-23T01:45:10.456Z" }, + { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507, upload-time = "2025-04-23T01:45:12.474Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104, upload-time = "2025-04-23T01:45:15.104Z" }, + { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240, upload-time = "2025-04-23T01:45:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685, upload-time = "2025-04-23T01:45:21.387Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164, upload-time = "2025-04-23T01:45:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206, upload-time = "2025-04-23T01:45:26.361Z" }, + { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144, upload-time = "2025-04-23T01:45:28.939Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124, upload-time = "2025-04-23T01:45:31.361Z" }, + { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520, upload-time = "2025-04-23T01:45:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016, upload-time = "2025-04-23T01:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884, upload-time = "2025-04-23T01:45:39.291Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690, upload-time = "2025-04-23T01:45:42.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418, upload-time = "2025-04-23T01:45:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092, upload-time = "2025-04-23T01:45:48.943Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231, upload-time = "2025-04-23T01:45:51.481Z" }, + { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798, upload-time = "2025-04-23T01:45:54.146Z" }, + { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195, upload-time = "2025-04-23T01:45:56.685Z" }, + { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243, upload-time = "2025-04-23T01:45:58.863Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, + { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, + { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, + { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, + { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, + { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, + { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, + { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319, upload-time = "2025-04-23T01:49:22.069Z" }, + { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614, upload-time = "2025-04-23T01:49:24.599Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273, upload-time = "2025-04-23T01:49:27.355Z" }, + { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552, upload-time = "2025-04-23T01:49:29.949Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091, upload-time = "2025-04-23T01:49:32.842Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862, upload-time = "2025-04-23T01:49:36.296Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "memory-profiler" +version = "0.61.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/88/e1907e1ca3488f2d9507ca8b0ae1add7b1cd5d3ca2bc8e5b329382ea2c7b/memory_profiler-0.61.0.tar.gz", hash = "sha256:4e5b73d7864a1d1292fb76a03e82a3e78ef934d06828a698d9dada76da2067b0", size = 35935, upload-time = "2022-11-15T17:57:28.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/26/aaca612a0634ceede20682e692a6c55e35a94c21ba36b807cc40fe910ae1/memory_profiler-0.61.0-py3-none-any.whl", hash = "sha256:400348e61031e3942ad4d4109d18753b2fb08c2f6fb8290671c5513a34182d84", size = 31803, upload-time = "2022-11-15T17:57:27.031Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, + { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, + { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, +] + +[[package]] +name = "nltk" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/49/51af17a2b0d850578d0022408802aa452644d40281a6c6e82f7cb0235ddb/nltk-3.8.1.zip", hash = "sha256:1834da3d0682cba4f2cede2f9aad6b0fafb6461ba451db0efb6f9c39798d64d3", size = 4620388, upload-time = "2023-01-02T15:37:09.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/0a/0d20d2c0f16be91b9fa32a77b76c60f9baf6eba419e5ef5deca17af9c582/nltk-3.8.1-py3-none-any.whl", hash = "sha256:fd5c9109f976fa86bcadba8f91e47f5e9293bd034474752e92a520f81c93dda5", size = 1510663, upload-time = "2023-01-02T15:37:07.414Z" }, +] + +[[package]] +name = "nose" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98", size = 280488, upload-time = "2015-06-02T09:12:32.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/d8/dd071918c040f50fa1cf80da16423af51ff8ce4a0f2399b7bf8de45ac3d9/nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", size = 154731, upload-time = "2015-06-02T09:12:40.57Z" }, +] + +[[package]] +name = "num2words" +version = "0.5.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docopt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/58/ad645bd38b4b648eb2fc2ba1b909398e54eb0cbb6a7dbd2b4953e38c9621/num2words-0.5.14.tar.gz", hash = "sha256:b066ec18e56b6616a3b38086b5747daafbaa8868b226a36127e0451c0cf379c6", size = 218213, upload-time = "2024-12-17T20:17:10.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/5b/545e9267a1cc080c8a1be2746113a063e34bcdd0f5173fd665a5c13cb234/num2words-0.5.14-py3-none-any.whl", hash = "sha256:1c8e5b00142fc2966fd8d685001e36c4a9911e070d1b120e1beb721fa1edb33d", size = 163525, upload-time = "2024-12-17T20:17:06.074Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/ee/146cab1ff6d575b54ace8a6a5994048380dc94879b0125b25e62edcb9e52/pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1", size = 5203060, upload-time = "2023-01-19T08:31:39.615Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/cd/34f6b0780301be81be804d7aa71d571457369e6131e2b330af2b0fed1aad/pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406", size = 18619230, upload-time = "2023-01-19T08:29:07.301Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/b7858bb7d6d6bf4d9df1dde777a11fcf3ff370e1d1b3956e3d0fcca8322c/pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572", size = 11982991, upload-time = "2023-01-19T08:29:15.383Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6c/005bd604994f7cbede4d7bf030614ef49a2213f76bc3d738ecf5b0dcc810/pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996", size = 10927131, upload-time = "2023-01-19T08:29:20.342Z" }, + { url = "https://files.pythonhosted.org/packages/27/c7/35b81ce5f680f2dac55eac14d103245cd8cf656ae4a2ff3be2e69fd1d330/pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354", size = 11368188, upload-time = "2023-01-19T08:29:25.807Z" }, + { url = "https://files.pythonhosted.org/packages/49/e2/79e46612dc25ebc7603dc11c560baa7266c90f9e48537ecf1a02a0dd6bff/pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23", size = 12062104, upload-time = "2023-01-19T08:29:30.695Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/f27c2992cbe05a3e39937f73a4be635a9ec149ec3ca4467d8cf039718994/pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328", size = 10362473, upload-time = "2023-01-19T08:29:37.506Z" }, + { url = "https://files.pythonhosted.org/packages/e2/24/a26af514113fd5eca2d8fe41ba4f22f70dfe6afefde4a6beb6a203570935/pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc", size = 18387750, upload-time = "2023-01-19T08:29:43.119Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/d2f910dace7ef849b626980d0fd033b9cded36568949c8d560c9630ad2e0/pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d", size = 11868668, upload-time = "2023-01-19T08:29:48.733Z" }, + { url = "https://files.pythonhosted.org/packages/b0/be/1843b9aff84b98899663e7cad9f45513dfdd11d69cb5bd85c648aaf6a8d4/pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc", size = 10814036, upload-time = "2023-01-19T08:29:54.886Z" }, + { url = "https://files.pythonhosted.org/packages/63/8d/c2bd356b9d4baf1c5cf8d7e251fb4540e87083072c905430da48c2bb31eb/pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae", size = 11374218, upload-time = "2023-01-19T08:30:00.5Z" }, + { url = "https://files.pythonhosted.org/packages/56/73/3351beeb807dca69fcc3c4966bcccc51552bd01549a9b13c04ab00a43f21/pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6", size = 12017319, upload-time = "2023-01-19T08:30:06.097Z" }, + { url = "https://files.pythonhosted.org/packages/da/6d/1235da14daddaa6e47f74ba0c255358f0ce7a6ee05da8bf8eb49161aa6b5/pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003", size = 10303385, upload-time = "2023-01-19T08:30:11.148Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/3e/c81eb24fb98b00bc097b9ce5dee6eca020c52619ed95b246aaec2018c511/platformdirs-4.7.1.tar.gz", hash = "sha256:6f4ff8472e482af4b7e67a183fbe63da846a9b34f57d5019c4d112a181003d82", size = 25254, upload-time = "2026-02-13T17:57:16.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/82/36dcca0cfa42ad259bf0eb1f7af0d5759710c21928856891631cb47e4771/platformdirs-4.7.1-py3-none-any.whl", hash = "sha256:06ac79ae0c5025949f62711e3f7cd178736515a29bcc669f42a216016cd1dc7a", size = 19070, upload-time = "2026-02-13T17:57:15.67Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "psutil" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502, upload-time = "2024-12-19T18:21:20.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511, upload-time = "2024-12-19T18:21:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985, upload-time = "2024-12-19T18:21:49.254Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488, upload-time = "2024-12-19T18:21:51.638Z" }, + { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477, upload-time = "2024-12-19T18:21:55.306Z" }, + { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017, upload-time = "2024-12-19T18:21:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602, upload-time = "2024-12-19T18:22:08.808Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" }, +] + +[[package]] +name = "pycountry" +version = "24.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/57/c389fa68c50590881a75b7883eeb3dc15e9e73a0fdc001cdd45c13290c92/pycountry-24.6.1.tar.gz", hash = "sha256:b61b3faccea67f87d10c1f2b0fc0be714409e8fcdcc1315613174f6466c10221", size = 6043910, upload-time = "2024-06-01T04:12:15.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/ec/1fb891d8a2660716aadb2143235481d15ed1cbfe3ad669194690b0604492/pycountry-24.6.1-py3-none-any.whl", hash = "sha256:f1a4fb391cd7214f8eefd39556d740adcc233c778a27f8942c8dca351d6ce06f", size = 6335189, upload-time = "2024-06-01T04:11:49.711Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pylint" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425, upload-time = "2025-11-30T13:29:02.53Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + +[[package]] +name = "regex" +version = "2022.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/17/21e7195da87bcdbdd4ec32fdfdc87ccd5cf97b0a18e0f08ecacbaefca284/regex-2022.3.2.tar.gz", hash = "sha256:79e5af1ff258bc0fe0bdd6f69bc4ae33935a898e3cbefbbccf22e88a27fa053b", size = 383148, upload-time = "2022-03-02T02:10:13.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/f7/1540c50e80f4bdc0646cdaedbc20841d997950e82fe5a38cf3457b40b20c/regex-2022.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab69b4fe09e296261377d209068d52402fb85ef89dc78a9ac4a29a895f4e24a7", size = 288962, upload-time = "2022-03-02T02:06:45.229Z" }, + { url = "https://files.pythonhosted.org/packages/fd/da/9859b2230cb9420a63b01deb5a87abcdfa6b879686535edb0fafb5139b98/regex-2022.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5bc5f921be39ccb65fdda741e04b2555917a4bced24b4df14eddc7569be3b493", size = 281832, upload-time = "2022-03-02T02:06:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/84/7c/9f1d3fc7763782690092b5e6b48f56fb5f925eee1a6174cbc9d2549d4d99/regex-2022.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43eba5c46208deedec833663201752e865feddc840433285fbadee07b84b464d", size = 761276, upload-time = "2022-03-02T02:06:50.136Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/69a593b21e175bbb9833f65b6917ad4052010b905651320548ae1e9b583e/regex-2022.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c68d2c04f7701a418ec2e5631b7f3552efc32f6bcc1739369c6eeb1af55f62e0", size = 801056, upload-time = "2022-03-02T02:06:52.398Z" }, + { url = "https://files.pythonhosted.org/packages/36/0e/6b52e47e9c44e7ed07384208114442c742461d78b9558e19d069b8d0fe35/regex-2022.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:caa2734ada16a44ae57b229d45091f06e30a9a52ace76d7574546ab23008c635", size = 788566, upload-time = "2022-03-02T02:06:55.107Z" }, + { url = "https://files.pythonhosted.org/packages/70/f8/01e020980159de6b86092f00aa3965dae337c02b382750a605e324baad26/regex-2022.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef806f684f17dbd6263d72a54ad4073af42b42effa3eb42b877e750c24c76f86", size = 764246, upload-time = "2022-03-02T02:06:57.713Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/dcf3b43ad89516556bf76addd28b52ab808df1a7afedc91254003fd54a88/regex-2022.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be319f4eb400ee567b722e9ea63d5b2bb31464e3cf1b016502e3ee2de4f86f5c", size = 752957, upload-time = "2022-03-02T02:07:00.032Z" }, + { url = "https://files.pythonhosted.org/packages/32/47/3ff643c15b598c862ac1f65bda487cc3f817f6964e1f55b532d8e6c1e451/regex-2022.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:42bb37e2b2d25d958c25903f6125a41aaaa1ed49ca62c103331f24b8a459142f", size = 677716, upload-time = "2022-03-02T02:07:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/12e294f8aa365a87df17630607d6e0c76bd3cda0c3524fbad079b6b7f347/regex-2022.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fbc88d3ba402b5d041d204ec2449c4078898f89c4a6e6f0ed1c1a510ef1e221d", size = 732227, upload-time = "2022-03-02T02:07:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/a2/05/179704f4b3c71e24212703510718faf42993c519ca603055138bd249598c/regex-2022.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:91e0f7e7be77250b808a5f46d90bf0032527d3c032b2131b63dee54753a4d729", size = 721470, upload-time = "2022-03-02T02:07:08.225Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a2/0aff2b0e284c8d5978ee930ca34c539a2e4ae67e4dcadb1adb342f3ce225/regex-2022.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:cb3652bbe6720786b9137862205986f3ae54a09dec8499a995ed58292bdf77c2", size = 755958, upload-time = "2022-03-02T02:07:11.997Z" }, + { url = "https://files.pythonhosted.org/packages/96/8e/e38627a1bddc7e681fa05810251fb446866c3fee0170a6e7fd69f45bc917/regex-2022.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:878c626cbca3b649e14e972c14539a01191d79e58934e3f3ef4a9e17f90277f8", size = 756244, upload-time = "2022-03-02T02:07:16.355Z" }, + { url = "https://files.pythonhosted.org/packages/1c/23/68987b9ff772a3bfe2d00cef4afeb06fca3136e75a2732e3f9d81b881a5c/regex-2022.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6df070a986fc064d865c381aecf0aaff914178fdf6874da2f2387e82d93cc5bd", size = 732795, upload-time = "2022-03-02T02:07:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/7b/23dd6bfa2e55c9411d2ed581bd857a4820be9e10a434c562f6666b50f367/regex-2022.3.2-cp310-cp310-win32.whl", hash = "sha256:b549d851f91a4efb3e65498bd4249b1447ab6035a9972f7fc215eb1f59328834", size = 257951, upload-time = "2022-03-02T02:07:22.316Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7c/ef26f3c3a5ba3cc5e88584cc1d9d08d0ba6ac887815826dd0d453e2bb8a1/regex-2022.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:8babb2b5751105dc0aef2a2e539f4ba391e738c62038d8cb331c710f6b0f3da7", size = 274400, upload-time = "2022-03-02T02:07:24.834Z" }, +] + +[[package]] +name = "reporters-db" +version = "3.2.63" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/9b/24b9ff4f33b6ef3b95d8e1c4bcbb3e5ea5427233a1762482c9063968fc0f/reporters_db-3.2.63.tar.gz", hash = "sha256:041592d807191914a46149566e53c3a27f4cf92d23650f48dc0150fc76254588", size = 186320, upload-time = "2025-11-26T17:13:51.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/27/387a2c9b96bc819d5462a717ca4f6f2e764d9ec3fb9c4d80771d6b43e6fb/reporters_db-3.2.63-py3-none-any.whl", hash = "sha256:ba03e7b4413b5e76d53d36ef46230746fdbaf306ea87182a91497b313d059ac5", size = 186461, upload-time = "2025-11-26T17:13:50.753Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "14.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/fa/8e158d81e3602da1e7bafbd4987938bc003fe4b0f44d65681e7f8face95a/scikit-learn-1.2.2.tar.gz", hash = "sha256:8429aea30ec24e7a8c7ed8a3fa6213adf3814a6efbea09e16e0a0c71e1a1a3d7", size = 7269934, upload-time = "2023-03-09T09:57:57.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/21/ee21352f69a980614cb4193d68a64a83aa2c0f80183c9485d6d61821a922/scikit_learn-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99cc01184e347de485bf253d19fcb3b1a3fb0ee4cea5ee3c43ec0cc429b6d29f", size = 9107257, upload-time = "2023-03-09T09:57:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/5a/43/5c4d21217df6a033999ee531fdfd52809263727b4afb26f7196a8ec709ae/scikit_learn-1.2.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e6e574db9914afcb4e11ade84fab084536a895ca60aadea3041e85b8ac963edb", size = 8455656, upload-time = "2023-03-09T09:57:13.131Z" }, + { url = "https://files.pythonhosted.org/packages/48/92/a39d1c9e0a6cb5ed4112899ecca590138484356ba8c4274dde6c3893ff14/scikit_learn-1.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fe83b676f407f00afa388dd1fdd49e5c6612e551ed84f3b1b182858f09e987d", size = 9165302, upload-time = "2023-03-09T09:57:15.336Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1e/36d7609e84b50d4a2e5bc43cd5013d9ea885799e5813a1e9cf5bb1afd3f4/scikit_learn-1.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2642baa0ad1e8f8188917423dd73994bf25429f8893ddbe115be3ca3183584", size = 9625294, upload-time = "2023-03-09T09:57:17.519Z" }, + { url = "https://files.pythonhosted.org/packages/f4/4d/fe3b35e18407da4b386be58616bd0f941ea1762a6c6798267f3aa64ef5d5/scikit_learn-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ad66c3848c0a1ec13464b2a95d0a484fd5b02ce74268eaa7e0c697b904f31d6c", size = 8306029, upload-time = "2023-03-09T09:57:19.64Z" }, + { url = "https://files.pythonhosted.org/packages/27/4a/1afe473760b07663710a75437b795ef37362aebb8bf513ff3bbf78fbd0c6/scikit_learn-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfeaf8be72117eb61a164ea6fc8afb6dfe08c6f90365bde2dc16456e4bc8e45f", size = 9024742, upload-time = "2023-03-09T09:57:22.275Z" }, + { url = "https://files.pythonhosted.org/packages/2f/fd/9fcbe7fe94150e72d87120cbc462bde1971c3674e726b81f4a4c4fdfa8e1/scikit_learn-1.2.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:fe0aa1a7029ed3e1dcbf4a5bc675aa3b1bc468d9012ecf6c6f081251ca47f590", size = 8375105, upload-time = "2023-03-09T09:57:25.23Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/301594a8bb1cfeeb95dd86aa7dfedd31e93211940105429abddf0933cfff/scikit_learn-1.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:065e9673e24e0dc5113e2dd2b4ca30c9d8aa2fa90f4c0597241c93b63130d233", size = 9150797, upload-time = "2023-03-09T09:57:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/4c/64/a1e6e92b850b39200c82e3bc54d556b2c634b3904c39ac5cdb10b1c5765f/scikit_learn-1.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf036ea7ef66115e0d49655f16febfa547886deba20149555a41d28f56fd6d3c", size = 9562879, upload-time = "2023-03-09T09:57:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/db/98/169b46a84b48f92df2b5e163fce75d471f4df933f8b3d925a61133210776/scikit_learn-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8b0670d4224a3c2d596fd572fb4fa673b2a0ccfb07152688ebd2ea0b8c61025c", size = 8261146, upload-time = "2023-03-09T09:57:32.138Z" }, +] + +[[package]] +name = "scipy" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/a9/2bf119f3f9cff1f376f924e39cfae18dec92a1514784046d185731301281/scipy-1.10.1.tar.gz", hash = "sha256:2cf9dfb80a7b4589ba4c40ce7588986d6d5cebc5457cad2c2880f6bc2d42f3a5", size = 42407997, upload-time = "2023-02-19T21:20:13.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/ac/b1f1bbf7b01d96495f35be003b881f10f85bf6559efb6e9578da832c2140/scipy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7354fd7527a4b0377ce55f286805b34e8c54b91be865bac273f527e1b839019", size = 35093243, upload-time = "2023-02-19T20:33:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e5/452086ebed676ce4000ceb5eeeb0ee4f8c6f67c7e70fb9323a370ff95c1f/scipy-1.10.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4b3f429188c66603a1a5c549fb414e4d3bdc2a24792e061ffbd607d3d75fd84e", size = 28772969, upload-time = "2023-02-19T20:34:39.318Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/a1b119c869b79a2ab459b7f9fd7e2dea75a9c7d432e64e915e75586bd00b/scipy-1.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1553b5dcddd64ba9a0d95355e63fe6c3fc303a8fd77c7bc91e77d61363f7433f", size = 30886961, upload-time = "2023-02-19T20:35:33.724Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4b/3bacad9a166350cb2e518cea80ab891016933cc1653f15c90279512c5fa9/scipy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0ff64b06b10e35215abce517252b375e580a6125fd5fdf6421b98efbefb2d2", size = 34422544, upload-time = "2023-02-19T20:37:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e3/b06ac3738bf365e89710205a471abe7dceec672a51c244b469bc5d1291c7/scipy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:fae8a7b898c42dffe3f7361c40d5952b6bf32d10c4569098d276b4c547905ee1", size = 42484848, upload-time = "2023-02-19T20:39:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/053cd3669be0d474deae8fe5f757bff4c4f480b8a410231e0631c068873d/scipy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f1564ea217e82c1bbe75ddf7285ba0709ecd503f048cb1236ae9995f64217bd", size = 35003170, upload-time = "2023-02-19T20:40:53.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3e/d05b9de83677195886fb79844fcca19609a538db63b1790fa373155bc3cf/scipy-1.10.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d925fa1c81b772882aa55bcc10bf88324dadb66ff85d548c71515f6689c6dac5", size = 28717513, upload-time = "2023-02-19T20:42:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/b69746c50e44893da57a68457da3d7e5bb75f6a37fbace3769b70d017488/scipy-1.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaea0a6be54462ec027de54fca511540980d1e9eea68b2d5c1dbfe084797be35", size = 30687257, upload-time = "2023-02-19T20:43:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/21/cd/fe2d4af234b80dc08c911ce63fdaee5badcdde3e9bcd9a68884580652ef0/scipy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15a35c4242ec5f292c3dd364a7c71a61be87a3d4ddcc693372813c0b73c9af1d", size = 34124096, upload-time = "2023-02-19T20:45:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/65/76/903324159e4a3566e518c558aeb21571d642f781d842d8dd0fd9c6b0645a/scipy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:43b8e0bcb877faf0abfb613d51026cd5cc78918e9530e375727bf0625c82788f", size = 42238704, upload-time = "2023-02-19T20:47:26.366Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smart-open" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/9a/0a7acb748b86e2922982366d780ca4b16c33f7246fa5860d26005c97e4f3/smart_open-7.5.0.tar.gz", hash = "sha256:f394b143851d8091011832ac8113ea4aba6b92e6c35f6e677ddaaccb169d7cb9", size = 53920, upload-time = "2025-11-08T21:38:40.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/95/bc978be7ea0babf2fb48a414b6afaad414c6a9e8b1eafc5b8a53c030381a/smart_open-7.5.0-py3-none-any.whl", hash = "sha256:87e695c5148bbb988f15cec00971602765874163be85acb1c9fb8abc012e6599", size = 63940, upload-time = "2025-11-08T21:38:39.024Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tika" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/e2/28022574d12239d6c158f903c7697ebb55591a05bfd3177146b2c7cd72d2/tika-3.1.0.tar.gz", hash = "sha256:4c3a404c3d846437c942d6a6fd7b71d50285690fae5489aa8a6f00ff9ccd0fc7", size = 32697, upload-time = "2025-03-26T16:12:27.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c6/9b549ca412bb03ad64632f5e47a53dc1b56a267809d7d3f9ef6d5b3c0559/tika-3.1.0-py3-none-any.whl", hash = "sha256:c6171c947d6410813f236c988a1fde4a6ad11cbaa95ec4e700eb9aef7c848093", size = 38053, upload-time = "2025-03-26T16:12:26.301Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "unidecode" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "us" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jellyfish" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/04/04323aefa1871de30286d3decae7706481c73bd428cf0c08e158bfa259a6/us-2.0.2.tar.gz", hash = "sha256:cb11ad0d43deff3a1c3690c74f0c731cff5b862c73339df2edd91133e1496fbc", size = 14207, upload-time = "2020-04-29T05:14:01.493Z" } + +[[package]] +name = "wrapt" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/37/ae31f40bec90de2f88d9597d0b5281e23ffe85b893a47ca5d9c05c63a4f6/wrapt-2.1.1.tar.gz", hash = "sha256:5fdcb09bf6db023d88f312bd0767594b414655d58090fc1c46b3414415f67fac", size = 81329, upload-time = "2026-02-03T02:12:13.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/21/293b657a27accfbbbb6007ebd78af0efa2083dac83e8f523272ea09b4638/wrapt-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e927375e43fd5a985b27a8992327c22541b6dede1362fc79df337d26e23604f", size = 60554, upload-time = "2026-02-03T02:11:17.362Z" }, + { url = "https://files.pythonhosted.org/packages/25/e9/96dd77728b54a899d4ce2798d7b1296989ce687ed3c0cb917d6b3154bf5d/wrapt-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c99544b6a7d40ca22195563b6d8bc3986ee8bb82f272f31f0670fe9440c869", size = 61496, upload-time = "2026-02-03T02:12:54.732Z" }, + { url = "https://files.pythonhosted.org/packages/44/79/4c755b45df6ef30c0dd628ecfaa0c808854be147ca438429da70a162833c/wrapt-2.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2be3fa5f4efaf16ee7c77d0556abca35f5a18ad4ac06f0ef3904c3399010ce9", size = 113528, upload-time = "2026-02-03T02:12:26.405Z" }, + { url = "https://files.pythonhosted.org/packages/9f/63/23ce28f7b841217d9a6337a340fbb8d4a7fbd67a89d47f377c8550fa34aa/wrapt-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67c90c1ae6489a6cb1a82058902caa8006706f7b4e8ff766f943e9d2c8e608d0", size = 115536, upload-time = "2026-02-03T02:11:54.397Z" }, + { url = "https://files.pythonhosted.org/packages/23/7b/5ca8d3b12768670d16c8329e29960eedd56212770365a02a8de8bf73dc01/wrapt-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05c0db35ccffd7480143e62df1e829d101c7b86944ae3be7e4869a7efa621f53", size = 114716, upload-time = "2026-02-03T02:12:20.771Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3a/9789ccb14a096d30bb847bf3ee137bf682cc9750c2ce155f4c5ae1962abf/wrapt-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0c2ec9f616755b2e1e0bf4d0961f59bb5c2e7a77407e7e2c38ef4f7d2fdde12c", size = 113200, upload-time = "2026-02-03T02:12:07.688Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e5/4ec3526ce6ce920b267c8d35d2c2f0874d3fad2744c8b7259353f1132baa/wrapt-2.1.1-cp310-cp310-win32.whl", hash = "sha256:203ba6b3f89e410e27dbd30ff7dccaf54dcf30fda0b22aa1b82d560c7f9fe9a1", size = 57876, upload-time = "2026-02-03T02:11:42.61Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4e/661c7c76ecd85375b2bc03488941a3a1078642af481db24949e2b9de01f4/wrapt-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:6f9426d9cfc2f8732922fc96198052e55c09bb9db3ddaa4323a18e055807410e", size = 60224, upload-time = "2026-02-03T02:11:19.096Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b7/53c7252d371efada4cb119e72e774fa2c6b3011fc33e3e552cdf48fb9488/wrapt-2.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:69c26f51b67076b40714cff81bdd5826c0b10c077fb6b0678393a6a2f952a5fc", size = 58645, upload-time = "2026-02-03T02:12:10.396Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a8/9254e4da74b30a105935197015b18b31b7a298bf046e67d8952ef74967bd/wrapt-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c366434a7fb914c7a5de508ed735ef9c133367114e1a7cb91dfb5cd806a1549", size = 60554, upload-time = "2026-02-03T02:11:13.038Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a1/378579880cc7af226354054a2c255f69615b379d8adad482bfe2f22a0dc2/wrapt-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d6a2068bd2e1e19e5a317c8c0b288267eec4e7347c36bc68a6e378a39f19ee7", size = 61491, upload-time = "2026-02-03T02:12:56.077Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/957b51c56acca35701665878ad31626182199fc4afecfe67dea072210f95/wrapt-2.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:891ab4713419217b2aed7dd106c9200f64e6a82226775a0d2ebd6bef2ebd1747", size = 113949, upload-time = "2026-02-03T02:11:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/cd/74/36bbebb4a3d2ae9c3e6929639721f8606cd0710a82a777c371aa69e36504/wrapt-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8ef36a0df38d2dc9d907f6617f89e113c5892e0a35f58f45f75901af0ce7d81", size = 115989, upload-time = "2026-02-03T02:12:19.398Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0d/f1177245a083c7be284bc90bddfe5aece32cdd5b858049cb69ce001a0e8d/wrapt-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76e9af3ebd86f19973143d4d592cbf3e970cf3f66ddee30b16278c26ae34b8ab", size = 115242, upload-time = "2026-02-03T02:11:08.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/3e/3b7cf5da27e59df61b1eae2d07dd03ff5d6f75b5408d694873cca7a8e33c/wrapt-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ff562067485ebdeaef2fa3fe9b1876bc4e7b73762e0a01406ad81e2076edcebf", size = 113676, upload-time = "2026-02-03T02:12:41.026Z" }, + { url = "https://files.pythonhosted.org/packages/f7/65/8248d3912c705f2c66f81cb97c77436f37abcbedb16d633b5ab0d795d8cd/wrapt-2.1.1-cp311-cp311-win32.whl", hash = "sha256:9e60a30aa0909435ec4ea2a3c53e8e1b50ac9f640c0e9fe3f21fd248a22f06c5", size = 57863, upload-time = "2026-02-03T02:12:18.112Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/d29310ab335f71f00c50466153b3dc985aaf4a9fc03263e543e136859541/wrapt-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7d79954f51fcf84e5ec4878ab4aea32610d70145c5bbc84b3370eabfb1e096c2", size = 60224, upload-time = "2026-02-03T02:12:29.289Z" }, + { url = "https://files.pythonhosted.org/packages/0c/90/a6ec319affa6e2894962a0cb9d73c67f88af1a726d15314bfb5c88b8a08d/wrapt-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:d3ffc6b0efe79e08fd947605fd598515aebefe45e50432dc3b5cd437df8b1ada", size = 58643, upload-time = "2026-02-03T02:12:43.022Z" }, + { url = "https://files.pythonhosted.org/packages/df/cb/4d5255d19bbd12be7f8ee2c1fb4269dddec9cef777ef17174d357468efaa/wrapt-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab8e3793b239db021a18782a5823fcdea63b9fe75d0e340957f5828ef55fcc02", size = 61143, upload-time = "2026-02-03T02:11:46.313Z" }, + { url = "https://files.pythonhosted.org/packages/6f/07/7ed02daa35542023464e3c8b7cb937fa61f6c61c0361ecf8f5fecf8ad8da/wrapt-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c0300007836373d1c2df105b40777986accb738053a92fe09b615a7a4547e9f", size = 61740, upload-time = "2026-02-03T02:12:51.966Z" }, + { url = "https://files.pythonhosted.org/packages/c4/60/a237a4e4a36f6d966061ccc9b017627d448161b19e0a3ab80a7c7c97f859/wrapt-2.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2b27c070fd1132ab23957bcd4ee3ba707a91e653a9268dc1afbd39b77b2799f7", size = 121327, upload-time = "2026-02-03T02:11:06.796Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fe/9139058a3daa8818fc67e6460a2340e8bbcf3aef8b15d0301338bbe181ca/wrapt-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b0e36d845e8b6f50949b6b65fc6cd279f47a1944582ed4ec8258cd136d89a64", size = 122903, upload-time = "2026-02-03T02:12:48.657Z" }, + { url = "https://files.pythonhosted.org/packages/91/10/b8479202b4164649675846a531763531f0a6608339558b5a0a718fc49a8d/wrapt-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4aeea04a9889370fcfb1ef828c4cc583f36a875061505cd6cd9ba24d8b43cc36", size = 121333, upload-time = "2026-02-03T02:11:32.148Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/75fc793b791d79444aca2c03ccde64e8b99eda321b003f267d570b7b0985/wrapt-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d88b46bb0dce9f74b6817bc1758ff2125e1ca9e1377d62ea35b6896142ab6825", size = 120458, upload-time = "2026-02-03T02:11:16.039Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8f/c3f30d511082ca6d947c405f9d8f6c8eaf83cfde527c439ec2c9a30eb5ea/wrapt-2.1.1-cp312-cp312-win32.whl", hash = "sha256:63decff76ca685b5c557082dfbea865f3f5f6d45766a89bff8dc61d336348833", size = 58086, upload-time = "2026-02-03T02:12:35.041Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c8/37625b643eea2849f10c3b90f69c7462faa4134448d4443234adaf122ae5/wrapt-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b828235d26c1e35aca4107039802ae4b1411be0fe0367dd5b7e4d90e562fcbcd", size = 60328, upload-time = "2026-02-03T02:12:45.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/79/56242f07572d5682ba8065a9d4d9c2218313f576e3c3471873c2a5355ffd/wrapt-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:75128507413a9f1bcbe2db88fd18fbdbf80f264b82fa33a6996cdeaf01c52352", size = 58722, upload-time = "2026-02-03T02:12:27.949Z" }, + { url = "https://files.pythonhosted.org/packages/c4/da/5a086bf4c22a41995312db104ec2ffeee2cf6accca9faaee5315c790377d/wrapt-2.1.1-py3-none-any.whl", hash = "sha256:3b0f4629eb954394a3d7c7a1c8cca25f0b07cefe6aa8545e862e9778152de5b7", size = 43886, upload-time = "2026-02-03T02:11:45.048Z" }, +] + +[[package]] +name = "zahlwort2num" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/c9/0270d78b7632faff3f74597c7a00b8d2fa4c88659d7bf9b2508f69a62f45/zahlwort2num-0.4.3.tar.gz", hash = "sha256:98cf4c797d27373e4bac675a248bb8ef56f5c9ed51255dc8bf254d5d498bdacd", size = 9200, upload-time = "2026-01-22T10:47:14.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/82/2c911a8de734fb96673c23073cd6c30b61b69688fb4d240e6e2495b2f98a/zahlwort2num-0.4.3-py3-none-any.whl", hash = "sha256:85d1c84557a36e8389a011c03e19197120e55cbd693ba10d18ae8425574640ed", size = 8906, upload-time = "2026-01-22T10:47:12.526Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 18d506a7fd43e2fb90a3a9eb62fa62b94eec384f Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 01:30:12 +0000 Subject: [PATCH 03/38] build: harden packaging artifacts and update migration notes --- .github/workflows/ci.yml | 4 +- AGENTS.md | 3 ++ MANIFEST.in | 15 ++++-- MIGRATION_RUNBOOK.md | 1 + Pipfile | 3 ++ README.rst | 23 ++++++++- ci/check_dist_contents.py | 94 ++++++++++++++++++++++++++++++++++++ notes.md | 91 ++++++++++++++++++++++++++++++++++ python-requirements-dev.txt | 6 ++- python-requirements-full.txt | 4 ++ python-requirements.txt | 4 ++ 11 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 ci/check_dist_contents.py create mode 100644 notes.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f4e827..7d0a7cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,9 +97,11 @@ jobs: - name: Build source and wheel artifacts run: | - uv venv .venv-build --python "${PYTHON_VERSION}" uv build + - name: Validate artifact contents + run: python3 ci/check_dist_contents.py + - name: Install wheel in clean env run: | uv venv .venv-smoke --python "${PYTHON_VERSION}" diff --git a/AGENTS.md b/AGENTS.md index 9ccf2f3..2900130 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,6 +139,9 @@ Note: a single monolithic `LEXNLP_USE_STANFORD=true` run can occasionally hang i # quick dependency sanity ./.venv/bin/pip check +# packaging content sanity +python3 ci/check_dist_contents.py + # run one file ./.venv/bin/pytest lexnlp/extract/en/tests/test_dates.py diff --git a/MANIFEST.in b/MANIFEST.in index 983e9b3..7d3aa75 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,11 +1,18 @@ include README.md include README.rst include index.rst -include Pipfile -include Pipfile.lock +exclude Pipfile +exclude Pipfile.lock +exclude python-requirements.txt +exclude python-requirements-dev.txt +exclude python-requirements-full.txt recursive-include lexnlp *.pickle recursive-include lexnlp/extract/en/addresses *.json *.txt *.xml recursive-include lexnlp *.csv -recursive-include libs * -recursive-include scripts * +include libs/download_stanford_nlp.sh +include libs/download_wiki.sh +recursive-include scripts *.py *.sh recursive-include documentation * +prune libs/stanford_nlp +global-exclude __pycache__ +global-exclude *.py[cod] diff --git a/MIGRATION_RUNBOOK.md b/MIGRATION_RUNBOOK.md index fef001a..7c78a16 100644 --- a/MIGRATION_RUNBOOK.md +++ b/MIGRATION_RUNBOOK.md @@ -63,6 +63,7 @@ Passing both commands is the required 100% result for a fully provisioned enviro ```bash uv build +python3 ci/check_dist_contents.py uv venv --python 3.11 .venv-smoke uv pip install --python .venv-smoke/bin/python dist/*.whl .venv-smoke/bin/python -c "import lexnlp; print(lexnlp.__version__)" diff --git a/Pipfile b/Pipfile index 31e0807..bce5e80 100644 --- a/Pipfile +++ b/Pipfile @@ -1,3 +1,6 @@ +# DEPRECATED: Legacy pipenv manifest retained only for historical reproduction. +# Use pyproject.toml + uv.lock for all active development and CI workflows. + [[source]] url = "https://pypi.org/simple" verify_ssl = true diff --git a/README.rst b/README.rst index 4d12bbd..b8fa1d0 100644 --- a/README.rst +++ b/README.rst @@ -75,8 +75,27 @@ terms or a non-GPL evaluation license by contacting ContraxSuite Licensing at Requirements ------------ -- Python 3.8 -- pipenv +- Python 3.11 (default; supported range is defined in ``pyproject.toml``) +- ``uv`` + +Quick Setup (uv + pyproject) +---------------------------- + +.. code:: bash + + cd /Users/jackeames/Downloads/LexNLP + uv python install 3.11 + uv venv --python 3.11 .venv + uv pip install --python .venv/bin/python -e ".[dev,test]" + ./.venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model + +Deprecated Setup Variants +------------------------- + +``Pipfile``, ``python-requirements.txt``, ``python-requirements-dev.txt``, +and ``python-requirements-full.txt`` are deprecated and kept only for +legacy reproduction. New development and CI updates should use ``uv`` with +``pyproject.toml``. Releases -------- diff --git a/ci/check_dist_contents.py b/ci/check_dist_contents.py new file mode 100644 index 0000000..a8096f2 --- /dev/null +++ b/ci/check_dist_contents.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Validate built distribution contents.""" + +from __future__ import annotations + +import sys +import tarfile +import zipfile +from pathlib import Path +from typing import Iterable, List + +BANNED_SUBSTRINGS = ( + "libs/stanford_nlp/", + "scripts/__pycache__/", +) + +BANNED_BASENAMES = ( + "Pipfile", + "Pipfile.lock", + "python-requirements.txt", + "python-requirements-dev.txt", + "python-requirements-full.txt", +) + +BANNED_SUFFIXES = ( + ".pyc", + ".pyo", +) + + +def iter_tar_names(path: Path) -> Iterable[str]: + with tarfile.open(path, "r:*") as archive: + for member in archive.getmembers(): + if member.isfile(): + yield member.name + + +def iter_zip_names(path: Path) -> Iterable[str]: + with zipfile.ZipFile(path) as archive: + for name in archive.namelist(): + if not name.endswith("/"): + yield name + + +def find_violations(names: Iterable[str]) -> List[str]: + violations: List[str] = [] + for name in names: + normalized = name.replace("\\", "/") + if any(token in normalized for token in BANNED_SUBSTRINGS): + violations.append(normalized) + continue + if normalized.rsplit("/", 1)[-1] in BANNED_BASENAMES: + violations.append(normalized) + continue + if normalized.endswith(BANNED_SUFFIXES): + violations.append(normalized) + return violations + + +def main(argv: list[str]) -> int: + dist_dir = Path(argv[0]) if argv else Path("dist") + if not dist_dir.exists(): + print(f"dist-check: missing dist directory: {dist_dir}", file=sys.stderr) + return 1 + + artifacts = sorted( + [*dist_dir.glob("*.whl"), *dist_dir.glob("*.tar.gz")] + ) + if not artifacts: + print(f"dist-check: no build artifacts found under {dist_dir}", file=sys.stderr) + return 1 + + failures: List[str] = [] + for artifact in artifacts: + if artifact.suffix == ".whl": + names = iter_zip_names(artifact) + else: + names = iter_tar_names(artifact) + violations = find_violations(names) + for violation in violations: + failures.append(f"{artifact.name}: {violation}") + + if failures: + print("dist-check: forbidden files found in artifacts", file=sys.stderr) + for failure in failures: + print(f" - {failure}", file=sys.stderr) + return 1 + + print("dist-check: OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..99ee128 --- /dev/null +++ b/notes.md @@ -0,0 +1,91 @@ +# LexNLP Dependency Modernization Notes + +Date: February 14, 2026 +Repo: `/Users/jackeames/Downloads/LexNLP` + +## Objective + +Modernize dependency and test tooling so LexNLP is reproducible with `uv`, Python 3.11, and a strict no-test-bypass policy while keeping behavior stable. + +## What was required + +1. Move install/runtime metadata to `pyproject.toml` and lock with `uv.lock`. +2. Align dependency versions to combinations that build and run on modern Python. +3. Make optional runtime assets deterministic (NLTK, contract model, Stanford, optional Tika). +4. Enforce test integrity (no new skip/xfail bypasses). +5. Verify packaging artifacts are clean and installable. + +## Issues encountered and how they were fixed + +### 1) Legacy dependency pins were not viable on current interpreters/toolchains +- Problem: historical pins were brittle on newer Python/packaging tooling. +- Fix: standardized on Python 3.11 and curated compatible ranges in `pyproject.toml`, then generated and used `uv.lock` in CI/local runs. + +### 2) Serialized sklearn artifacts were loaded under newer sklearn +- Problem: older serialized models triggered compatibility warnings and runtime attribute mismatches (notably GaussianNB legacy attributes). +- Fix: pinned to `scikit-learn>=1.2.2,<1.3` and added compatibility handling in `lexnlp/ml/predictor.py` for legacy `sigma_` data. + +### 3) Pandas API drift broke a parser path +- Problem: deprecated `error_bad_lines` behavior caused incompatibility on modern pandas. +- Fix: updated `lexnlp/extract/es/regulations.py` to use `on_bad_lines='skip'` with backward-compatible fallback. + +### 4) Test reliability depended on non-Python assets +- Problem: failing/skipped tests when corpora/models/Stanford jars were missing. +- Fix: introduced `scripts/bootstrap_assets.py` with deterministic flags for: + - NLTK corpora + - contract model artifact + - Stanford jars/models (plus Java requirement) + - optional Tika + +### 5) Skips/xfails could hide regressions +- Problem: policy needed to prevent making builds green by bypassing tests. +- Fix: added `ci/skip_audit.py` and `ci/skip_audit_allowlist.txt`, wired into CI to fail on unapproved new skip/xfail markers. + +### 6) Packaging artifacts included unwanted content risk +- Problem: release artifacts could unintentionally include local runtime blobs (`libs/stanford_nlp`), bytecode caches, and deprecated manifest files. +- Fix: + - tightened `MANIFEST.in` + - added `ci/check_dist_contents.py` + - enforced artifact content audit in CI packaging job + +## Final validation results + +- Skip audit: `skip-audit: OK (markers=11, allowlisted=11, annotated_new=0)` +- Base suite (`./.venv311/bin/python -m pytest -q`): `497 passed, 11 skipped, 0 failed` +- Stanford-gated suite (`LEXNLP_USE_STANFORD=true` targeted files): `11 passed, 0 failed` +- Net demonstrated pass count: `508/508` +- Packaging: + - `uv build` succeeds + - `python3 ci/check_dist_contents.py` succeeds + - wheel install smoke test succeeds and imports `lexnlp==2.3.0` + +## Operational guidance + +Reliable full-validation flow on this machine: + +```bash +cd /Users/jackeames/Downloads/LexNLP + +# env +uv venv --python 3.11 .venv311 +uv sync --frozen --python .venv311/bin/python --extra dev --extra test + +# assets +./.venv311/bin/python scripts/bootstrap_assets.py --nltk --contract-model +./.venv311/bin/python scripts/bootstrap_assets.py --stanford + +# base tests +./.venv311/bin/python -m pytest -q + +# stanford-only tests +PATH=/opt/homebrew/opt/openjdk/bin:$PATH \ +LEXNLP_USE_STANFORD=true \ +./.venv311/bin/python -m pytest -q \ + lexnlp/nlp/en/tests/test_stanford.py \ + lexnlp/extract/en/entities/tests/test_stanford_ner.py + +# policy + packaging +python3 ci/skip_audit.py +uv build +python3 ci/check_dist_contents.py +``` diff --git a/python-requirements-dev.txt b/python-requirements-dev.txt index 60c6c6d..e150c05 100644 --- a/python-requirements-dev.txt +++ b/python-requirements-dev.txt @@ -1,3 +1,7 @@ +# DEPRECATED: Legacy snapshot retained for historical reproduction only. +# Use pyproject.toml + uv.lock via: +# uv sync --frozen --extra dev --extra test + beautifulsoup4==4.11.1 cloudpickle==2.1.0 dateparser==1.1.1 @@ -31,4 +35,4 @@ tika==1.24 twine==4.0.1 Unidecode==1.3.4 us==2.0.2 -zahlwort2num==0.3.0 \ No newline at end of file +zahlwort2num==0.3.0 diff --git a/python-requirements-full.txt b/python-requirements-full.txt index 6209c2a..b91ac56 100644 --- a/python-requirements-full.txt +++ b/python-requirements-full.txt @@ -1,3 +1,7 @@ +# DEPRECATED: Legacy snapshot retained for historical reproduction only. +# Use pyproject.toml + uv.lock via: +# uv sync --frozen --extra dev --extra test + beautifulsoup4==4.11.1 cloudpickle==2.1.0 coverage==6.4.1 diff --git a/python-requirements.txt b/python-requirements.txt index 04f69dd..f366c97 100755 --- a/python-requirements.txt +++ b/python-requirements.txt @@ -1,3 +1,7 @@ +# DEPRECATED: Legacy snapshot retained for historical reproduction only. +# Use pyproject.toml + uv.lock via: +# uv sync --frozen --extra dev --extra test + alabaster==0.7.12 astroid==2.12.12 attrs==22.1.0 From 49ab86a30bd3129a98c3013032731f2faf592996 Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 02:17:46 +0000 Subject: [PATCH 04/38] ci: enforce contract model quality gate with baseline metrics --- .github/workflows/ci.yml | 44 ++++++ AGENTS.md | 7 + MIGRATION_RUNBOOK.md | 23 +++- notes.md | 14 +- scripts/model_quality_gate.py | 126 +++++++++++++++++- .../is_contract_baseline_metrics.json | 11 ++ 6 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 test_data/model_quality/is_contract_baseline_metrics.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d0a7cc..50857a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,9 @@ permissions: env: PYTHON_VERSION: "3.11" + CONTRACT_MODEL_BASELINE_TAG: "pipeline/is-contract/0.1" + CONTRACT_MODEL_CANDIDATE_TAG: "pipeline/is-contract/0.1" + CONTRACT_MODEL_BASELINE_METRICS: "test_data/model_quality/is_contract_baseline_metrics.json" jobs: base-tests: @@ -79,6 +82,47 @@ jobs: lexnlp/nlp/en/tests/test_stanford.py \ lexnlp/extract/en/entities/tests/test_stanford_ner.py + model-quality: + name: Model Quality Gate + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Install dependencies + run: | + uv venv .venv --python "${PYTHON_VERSION}" + uv sync --frozen --python .venv/bin/python --extra test + + - name: Bootstrap contract-model asset + run: .venv/bin/python scripts/bootstrap_assets.py --contract-model + + - name: Run contract model quality gate + run: | + .venv/bin/python scripts/model_quality_gate.py \ + --baseline-tag "${CONTRACT_MODEL_BASELINE_TAG}" \ + --candidate-tag "${CONTRACT_MODEL_CANDIDATE_TAG}" \ + --baseline-metrics-json "${CONTRACT_MODEL_BASELINE_METRICS}" \ + --output-json artifacts/model_quality_gate.json \ + --max-f1-regression 0.0 \ + --max-accuracy-regression 0.0 + + - name: Upload quality-gate result + uses: actions/upload-artifact@v4 + with: + name: model-quality-gate + path: artifacts/model_quality_gate.json + if-no-files-found: error + packaging-smoke: name: Packaging Smoke runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 2900130..e47cbb6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -142,6 +142,12 @@ Note: a single monolithic `LEXNLP_USE_STANFORD=true` run can occasionally hang i # packaging content sanity python3 ci/check_dist_contents.py +# contract model quality gate (baseline metrics) +./.venv/bin/python scripts/model_quality_gate.py \ + --baseline-tag pipeline/is-contract/0.1 \ + --candidate-tag pipeline/is-contract/0.1 \ + --baseline-metrics-json test_data/model_quality/is_contract_baseline_metrics.json + # run one file ./.venv/bin/pytest lexnlp/extract/en/tests/test_dates.py @@ -163,5 +169,6 @@ python3 ci/check_dist_contents.py - Targeted tests for changed modules pass. - Full base run (`pytest lexnlp`) passes. - If Stanford assets are enabled, Stanford-only suite with `LEXNLP_USE_STANFORD=true` passes. +- Contract model quality gate passes against `test_data/model_quality/is_contract_baseline_metrics.json`. - No `skip`/`skipif`/`xfail` policy bypasses were introduced. - Document any required asset downloads (NLTK, pipeline models, Stanford, Tika) in PR notes. diff --git a/MIGRATION_RUNBOOK.md b/MIGRATION_RUNBOOK.md index 7c78a16..5eefd21 100644 --- a/MIGRATION_RUNBOOK.md +++ b/MIGRATION_RUNBOOK.md @@ -71,17 +71,36 @@ uv pip install --python .venv-smoke/bin/python dist/*.whl ## 7) Model Upgrade Quality Gate -Use the quality gate script before adopting a new contract-model artifact: +Use the quality gate script before adopting a new contract-model artifact. + +The committed baseline metrics file is: +- `test_data/model_quality/is_contract_baseline_metrics.json` + +Candidate evaluation command: ```bash ./.venv/bin/python scripts/model_quality_gate.py \ + --baseline-tag pipeline/is-contract/0.1 \ --candidate-tag pipeline/is-contract/0.2 \ + --baseline-metrics-json test_data/model_quality/is_contract_baseline_metrics.json \ --fixture test_data/lexnlp/extract/en/contracts/tests/test_contracts/test_is_contract.csv \ --max-f1-regression 0.0 \ --max-accuracy-regression 0.0 ``` -Default policy is non-regression against the baseline model (`pipeline/is-contract/0.1`). +Default policy is non-regression against baseline metrics from +`pipeline/is-contract/0.1` on the fixed fixture above. + +If baseline-tag model behavior is intentionally changed, regenerate and review +the baseline metrics file in the same PR: + +```bash +./.venv/bin/python scripts/model_quality_gate.py \ + --baseline-tag pipeline/is-contract/0.1 \ + --candidate-tag pipeline/is-contract/0.1 \ + --fixture test_data/lexnlp/extract/en/contracts/tests/test_contracts/test_is_contract.csv \ + --write-baseline-metrics-json test_data/model_quality/is_contract_baseline_metrics.json +``` ## 8) Failure Triage diff --git a/notes.md b/notes.md index 99ee128..7b05e2d 100644 --- a/notes.md +++ b/notes.md @@ -57,7 +57,19 @@ Modernize dependency and test tooling so LexNLP is reproducible with `uv`, Pytho - Packaging: - `uv build` succeeds - `python3 ci/check_dist_contents.py` succeeds - - wheel install smoke test succeeds and imports `lexnlp==2.3.0` +- wheel install smoke test succeeds and imports `lexnlp==2.3.0` + +## Follow-up completed (quality gate in CI) + +- Added committed baseline metrics fixture: + - `test_data/model_quality/is_contract_baseline_metrics.json` +- Extended `scripts/model_quality_gate.py` to: + - consume baseline metrics JSON directly + - validate fixture/min-probability alignment + - optionally write canonical baseline metrics JSON +- Added a dedicated GitHub Actions job `model-quality` in + - `.github/workflows/ci.yml` + - This runs the contract-model quality gate and uploads the JSON result as an artifact. ## Operational guidance diff --git a/scripts/model_quality_gate.py b/scripts/model_quality_gate.py index 84a7fa1..e4ebe40 100644 --- a/scripts/model_quality_gate.py +++ b/scripts/model_quality_gate.py @@ -8,12 +8,13 @@ import json import sys from pathlib import Path -from typing import Dict, List, Sequence, Tuple +from typing import Any, Dict, List, Sequence, Tuple DEFAULT_FIXTURE = Path( "test_data/lexnlp/extract/en/contracts/tests/test_contracts/test_is_contract.csv" ) +REQUIRED_METRIC_KEYS = ("accuracy", "f1", "precision", "recall") def parse_args(argv: Sequence[str]) -> argparse.Namespace: @@ -25,6 +26,21 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: default="pipeline/is-contract/0.1", help="Catalog tag used as baseline model.", ) + parser.add_argument( + "--baseline-metrics-json", + type=Path, + help=( + "Optional path to committed baseline metrics JSON. " + "When provided, baseline metrics are loaded from this file " + "instead of executing the baseline model." + ), + ) + parser.add_argument( + "--baseline-metrics-tolerance", + type=float, + default=1e-9, + help="Tolerance for fixture/probability checks when baseline metrics JSON is used.", + ) parser.add_argument( "--candidate-tag", required=True, @@ -65,6 +81,14 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: type=Path, help="Optional path to write JSON results.", ) + parser.add_argument( + "--write-baseline-metrics-json", + type=Path, + help=( + "Optional path to write canonical baseline metrics JSON " + "(baseline_tag, fixture, min_probability, metrics)." + ), + ) return parser.parse_args(argv) @@ -126,16 +150,89 @@ def score_pipeline(pipeline, texts: List[str], labels: List[bool], min_probabili } +def parse_metrics(raw: Dict[str, Any], source: str) -> Dict[str, float]: + missing = [key for key in REQUIRED_METRIC_KEYS if key not in raw] + if missing: + raise ValueError(f"Missing metric keys in {source}: {', '.join(missing)}") + + return { + key: float(raw[key]) + for key in REQUIRED_METRIC_KEYS + } + + +def load_baseline_metrics(path: Path) -> Dict[str, Any]: + if not path.exists(): + raise FileNotFoundError(f"Baseline metrics file not found: {path}") + + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError("Baseline metrics JSON must be an object") + + # Accept either a dedicated "metrics" object or the full quality-gate output shape. + if "metrics" in payload: + metrics = parse_metrics(payload["metrics"], f"{path}::metrics") + elif "baseline" in payload: + metrics = parse_metrics(payload["baseline"], f"{path}::baseline") + else: + raise ValueError( + f"Baseline metrics JSON must contain either 'metrics' or 'baseline': {path}" + ) + + return { + "metrics": metrics, + "baseline_tag": payload.get("baseline_tag"), + "fixture": payload.get("fixture"), + "min_probability": payload.get("min_probability"), + "raw": payload, + } + + def main(argv: Sequence[str]) -> int: args = parse_args(argv) texts, labels = load_fixture(args.fixture) - baseline_metrics = score_pipeline( - load_pipeline_for_tag(args.baseline_tag), - texts, - labels, - args.min_probability, - ) + baseline_source = "tag" + baseline_metrics_source = None + baseline_metrics_file: Dict[str, Any] | None = None + + if args.baseline_metrics_json: + baseline_source = "metrics-json" + baseline_metrics_source = str(args.baseline_metrics_json) + baseline_metrics_file = load_baseline_metrics(args.baseline_metrics_json) + baseline_metrics = baseline_metrics_file["metrics"] + + file_baseline_tag = baseline_metrics_file.get("baseline_tag") + if file_baseline_tag and file_baseline_tag != args.baseline_tag: + raise ValueError( + "Baseline tag mismatch between --baseline-tag and --baseline-metrics-json: " + f"{args.baseline_tag!r} != {file_baseline_tag!r}" + ) + + file_fixture = baseline_metrics_file.get("fixture") + if file_fixture: + expected_fixture = str(args.fixture) + if file_fixture != expected_fixture: + raise ValueError( + "Fixture mismatch between --fixture and --baseline-metrics-json: " + f"{expected_fixture!r} != {file_fixture!r}" + ) + + file_min_probability = baseline_metrics_file.get("min_probability") + if file_min_probability is not None: + if abs(float(file_min_probability) - args.min_probability) > args.baseline_metrics_tolerance: + raise ValueError( + "min_probability mismatch between CLI and baseline metrics JSON: " + f"{args.min_probability} != {file_min_probability}" + ) + else: + baseline_metrics = score_pipeline( + load_pipeline_for_tag(args.baseline_tag), + texts, + labels, + args.min_probability, + ) + candidate_metrics = score_pipeline( load_pipeline_for_tag(args.candidate_tag), texts, @@ -148,6 +245,8 @@ def main(argv: Sequence[str]) -> int: "candidate_tag": args.candidate_tag, "fixture": str(args.fixture), "min_probability": args.min_probability, + "baseline_source": baseline_source, + "baseline_metrics_json": baseline_metrics_source, "baseline": baseline_metrics, "candidate": candidate_metrics, } @@ -177,6 +276,19 @@ def main(argv: Sequence[str]) -> int: args.output_json.parent.mkdir(parents=True, exist_ok=True) args.output_json.write_text(json.dumps(result, indent=2, sort_keys=True), encoding="utf-8") + if args.write_baseline_metrics_json: + baseline_payload = { + "baseline_tag": args.baseline_tag, + "fixture": str(args.fixture), + "metrics": baseline_metrics, + "min_probability": args.min_probability, + } + args.write_baseline_metrics_json.parent.mkdir(parents=True, exist_ok=True) + args.write_baseline_metrics_json.write_text( + json.dumps(baseline_payload, indent=2, sort_keys=True), + encoding="utf-8", + ) + if violations: for violation in violations: print(f"QUALITY GATE VIOLATION: {violation}") diff --git a/test_data/model_quality/is_contract_baseline_metrics.json b/test_data/model_quality/is_contract_baseline_metrics.json new file mode 100644 index 0000000..bf299a4 --- /dev/null +++ b/test_data/model_quality/is_contract_baseline_metrics.json @@ -0,0 +1,11 @@ +{ + "baseline_tag": "pipeline/is-contract/0.1", + "fixture": "test_data/lexnlp/extract/en/contracts/tests/test_contracts/test_is_contract.csv", + "metrics": { + "accuracy": 1.0, + "f1": 1.0, + "precision": 1.0, + "recall": 1.0 + }, + "min_probability": 0.3 +} From 54be6a32370d96a442e61ff3110f42acade0d75b Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 02:30:46 +0000 Subject: [PATCH 05/38] feat: add contract model re-export workflow --- .gitignore | 3 +- AGENTS.md | 6 + MIGRATION_RUNBOOK.md | 13 ++ notes.md | 10 + scripts/reexport_contract_model.py | 281 +++++++++++++++++++++++++++++ 5 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 scripts/reexport_contract_model.py diff --git a/.gitignore b/.gitignore index 3a21bea..1907c67 100644 --- a/.gitignore +++ b/.gitignore @@ -110,7 +110,8 @@ ENV/ # mypy .mypy_cache/ +artifacts/ + benchmarks lexnlp/extract/en/contracts/data/*.model /.pytest_cache/ - diff --git a/AGENTS.md b/AGENTS.md index e47cbb6..b3d0167 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,6 +148,12 @@ python3 ci/check_dist_contents.py --candidate-tag pipeline/is-contract/0.1 \ --baseline-metrics-json test_data/model_quality/is_contract_baseline_metrics.json +# create a re-exported candidate model tag and validate it +./.venv/bin/python scripts/reexport_contract_model.py \ + --source-tag pipeline/is-contract/0.1 \ + --target-tag pipeline/is-contract/0.2 \ + --baseline-metrics-json test_data/model_quality/is_contract_baseline_metrics.json + # run one file ./.venv/bin/pytest lexnlp/extract/en/tests/test_dates.py diff --git a/MIGRATION_RUNBOOK.md b/MIGRATION_RUNBOOK.md index 5eefd21..e5d9908 100644 --- a/MIGRATION_RUNBOOK.md +++ b/MIGRATION_RUNBOOK.md @@ -76,6 +76,19 @@ Use the quality gate script before adopting a new contract-model artifact. The committed baseline metrics file is: - `test_data/model_quality/is_contract_baseline_metrics.json` +To create a modern candidate artifact by re-serializing the baseline model +under the current runtime (Python/scikit-learn), use: + +```bash +./.venv/bin/python scripts/reexport_contract_model.py \ + --source-tag pipeline/is-contract/0.1 \ + --target-tag pipeline/is-contract/0.2 \ + --baseline-metrics-json test_data/model_quality/is_contract_baseline_metrics.json +``` + +This writes model-export metadata to +`artifacts/model_reexports/pipeline__is-contract__0.2.metadata.json` by default. + Candidate evaluation command: ```bash diff --git a/notes.md b/notes.md index 7b05e2d..156be0a 100644 --- a/notes.md +++ b/notes.md @@ -71,6 +71,16 @@ Modernize dependency and test tooling so LexNLP is reproducible with `uv`, Pytho - `.github/workflows/ci.yml` - This runs the contract-model quality gate and uploads the JSON result as an artifact. +## Follow-up completed (model artifact refresh workflow) + +- Added `scripts/reexport_contract_model.py` to support deterministic re-serialization + of `pipeline/is-contract/0.1` into a new local catalog tag (for example + `pipeline/is-contract/0.2`) under the current runtime. +- The script writes per-tag metadata JSON and runs `scripts/model_quality_gate.py` + automatically unless `--skip-quality-gate` is passed. +- The script also compares legacy sklearn warning counts between source and + candidate artifacts to ensure warning behavior does not regress. + ## Operational guidance Reliable full-validation flow on this machine: diff --git a/scripts/reexport_contract_model.py b/scripts/reexport_contract_model.py new file mode 100644 index 0000000..7fcec4f --- /dev/null +++ b/scripts/reexport_contract_model.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +"""Re-export the contract classifier model under a new catalog tag.""" + +from __future__ import annotations + +import argparse +import json +import pickle +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Sequence + +from cloudpickle import load + + +DEFAULT_FIXTURE = Path( + "test_data/lexnlp/extract/en/contracts/tests/test_contracts/test_is_contract.csv" +) +DEFAULT_BASELINE_METRICS = Path( + "test_data/model_quality/is_contract_baseline_metrics.json" +) +LEGACY_WARNING_TOKEN = "Trying to unpickle estimator" + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Load an existing contract classifier tag and re-serialize it with " + "the current runtime under a new tag." + ) + ) + parser.add_argument( + "--source-tag", + default="pipeline/is-contract/0.1", + help="Source model tag to load from LexNLP catalog.", + ) + parser.add_argument( + "--target-tag", + required=True, + help="Destination model tag to write under LexNLP catalog.", + ) + parser.add_argument( + "--force", + action="store_true", + help="Overwrite destination artifact if it already exists.", + ) + parser.add_argument( + "--skip-quality-gate", + action="store_true", + help="Skip post-export quality-gate execution.", + ) + parser.add_argument( + "--fixture", + type=Path, + default=DEFAULT_FIXTURE, + help=f"Fixture CSV used by quality gate (default: {DEFAULT_FIXTURE}).", + ) + parser.add_argument( + "--baseline-metrics-json", + type=Path, + default=DEFAULT_BASELINE_METRICS, + help=( + "Baseline metrics JSON for quality gate. " + f"Default: {DEFAULT_BASELINE_METRICS}" + ), + ) + parser.add_argument( + "--max-accuracy-regression", + type=float, + default=0.0, + help="Maximum allowed candidate accuracy drop.", + ) + parser.add_argument( + "--max-f1-regression", + type=float, + default=0.0, + help="Maximum allowed candidate F1 drop.", + ) + parser.add_argument( + "--min-probability", + type=float, + default=0.3, + help="Probability threshold used by model quality gate.", + ) + parser.add_argument( + "--output-metadata-json", + type=Path, + help=( + "Optional explicit path for metadata JSON. " + "Defaults to artifacts/model_reexports/.metadata.json." + ), + ) + parser.add_argument( + "--max-legacy-warning-regression", + type=int, + default=0, + help=( + "Maximum allowed increase in legacy sklearn unpickle warning count " + "between source and candidate model." + ), + ) + return parser.parse_args(argv) + + +def ensure_tag_downloaded(tag: str) -> Path: + from lexnlp.ml.catalog import get_path_from_catalog + from lexnlp.ml.catalog.download import download_github_release + + try: + return get_path_from_catalog(tag) + except FileNotFoundError: + download_github_release(tag, prompt_user=False) + return get_path_from_catalog(tag) + + +def run_quality_gate( + *, + source_tag: str, + target_tag: str, + fixture: Path, + baseline_metrics_json: Path, + min_probability: float, + max_accuracy_regression: float, + max_f1_regression: float, +) -> None: + gate_script = Path(__file__).with_name("model_quality_gate.py") + cmd = [ + sys.executable, + str(gate_script), + "--baseline-tag", + source_tag, + "--candidate-tag", + target_tag, + "--fixture", + str(fixture), + "--min-probability", + str(min_probability), + "--max-accuracy-regression", + str(max_accuracy_regression), + "--max-f1-regression", + str(max_f1_regression), + ] + + if baseline_metrics_json.exists(): + cmd.extend(["--baseline-metrics-json", str(baseline_metrics_json)]) + + subprocess.run(cmd, check=True) + + +def get_legacy_warning_messages(model_path: Path) -> list[str]: + probe_code = """ +import json +import sys +import warnings +from pathlib import Path +from cloudpickle import load + +token = sys.argv[2] +path = Path(sys.argv[1]) +with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always") + with path.open("rb") as model_file: + load(model_file) + +messages = [ + str(item.message).splitlines()[0] + for item in captured + if token in str(item.message) +] +print(json.dumps(messages)) +""" + result = subprocess.run( + [sys.executable, "-c", probe_code, str(model_path), LEGACY_WARNING_TOKEN], + check=True, + capture_output=True, + text=True, + ) + return json.loads(result.stdout.strip() or "[]") + + +def main(argv: Sequence[str]) -> int: + args = parse_args(argv) + if args.source_tag == args.target_tag: + raise ValueError("--source-tag and --target-tag must differ") + + from lexnlp import __version__ as lexnlp_version + from lexnlp.extract.en.contracts.predictors import ProbabilityPredictorIsContract + from lexnlp.ml.catalog import CATALOG + from sklearn import __version__ as sklearn_version + + source_path = ensure_tag_downloaded(args.source_tag) + destination_dir = CATALOG / args.target_tag + destination_path = destination_dir / source_path.name + + if destination_path.exists() and not args.force: + raise FileExistsError( + f"Destination already exists: {destination_path} " + "(use --force to overwrite)." + ) + + destination_dir.mkdir(parents=True, exist_ok=True) + + with source_path.open("rb") as source_file: + pipeline = load(source_file) + + # Validate and apply runtime compatibility patches before re-serializing. + ProbabilityPredictorIsContract(pipeline=pipeline) + + # Use stdlib pickle for re-export so sklearn writes current runtime metadata. + with destination_path.open("wb") as destination_file: + pickle.dump(pipeline, destination_file) + + default_metadata_path = Path("artifacts/model_reexports") / ( + f"{args.target_tag.replace('/', '__')}.metadata.json" + ) + metadata_path = args.output_metadata_json or default_metadata_path + metadata_path.parent.mkdir(parents=True, exist_ok=True) + metadata_payload = { + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "source_tag": args.source_tag, + "target_tag": args.target_tag, + "source_model_path": str(source_path), + "target_model_path": str(destination_path), + "fixture": str(args.fixture), + "min_probability": args.min_probability, + "runtime": { + "python": sys.version.split()[0], + "scikit_learn": sklearn_version, + "lexnlp": lexnlp_version, + }, + } + metadata_path.write_text( + json.dumps(metadata_payload, indent=2, sort_keys=True), + encoding="utf-8", + ) + + print(f"re-export: wrote model to {destination_path}") + print(f"re-export: wrote metadata to {metadata_path}") + + source_legacy_warning_messages = get_legacy_warning_messages(source_path) + candidate_legacy_warning_messages = get_legacy_warning_messages(destination_path) + source_legacy_warning_count = len(source_legacy_warning_messages) + candidate_legacy_warning_count = len(candidate_legacy_warning_messages) + print( + "re-export: legacy sklearn warnings " + f"(source={source_legacy_warning_count}, candidate={candidate_legacy_warning_count})" + ) + + warning_regression = candidate_legacy_warning_count - source_legacy_warning_count + if warning_regression > args.max_legacy_warning_regression: + print( + "re-export: legacy warning regression exceeds threshold " + f"({warning_regression} > {args.max_legacy_warning_regression})" + ) + if candidate_legacy_warning_messages: + print("re-export: candidate legacy warnings:") + for message in candidate_legacy_warning_messages: + print(f" - {message}") + return 1 + + if args.skip_quality_gate: + print("re-export: skipping quality gate by request") + else: + run_quality_gate( + source_tag=args.source_tag, + target_tag=args.target_tag, + fixture=args.fixture, + baseline_metrics_json=args.baseline_metrics_json, + min_probability=args.min_probability, + max_accuracy_regression=args.max_accuracy_regression, + max_f1_regression=args.max_f1_regression, + ) + print("re-export: quality gate passed") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) From 2bb666deb9198e14900b4d1e3c704e4e048dc421 Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 02:33:17 +0000 Subject: [PATCH 06/38] models: refresh bundled date model artifact --- MIGRATION_RUNBOOK.md | 12 ++++++++++++ lexnlp/extract/en/date_model.pickle | Bin 60616 -> 56047 bytes notes.md | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/MIGRATION_RUNBOOK.md b/MIGRATION_RUNBOOK.md index e5d9908..2784aeb 100644 --- a/MIGRATION_RUNBOOK.md +++ b/MIGRATION_RUNBOOK.md @@ -115,6 +115,18 @@ the baseline metrics file in the same PR: --write-baseline-metrics-json test_data/model_quality/is_contract_baseline_metrics.json ``` +### Refresh bundled sklearn artifacts + +If bundled sklearn artifacts emit legacy-version warnings, re-serialize them on +the current runtime and re-run targeted tests. Example for date parser model: + +```bash +./.venv/bin/python - <<'PY' +import joblib +joblib.dump(joblib.load("lexnlp/extract/en/date_model.pickle"), "lexnlp/extract/en/date_model.pickle", compress=3) +PY +``` + ## 8) Failure Triage - `LookupError` for NLTK resources: diff --git a/lexnlp/extract/en/date_model.pickle b/lexnlp/extract/en/date_model.pickle index 47b941a05b05449635772a259fd4bcdfdb11caa3..c2ee81a8281e466b9e6cf07a6eb92e6caa75863d 100644 GIT binary patch literal 56047 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 WFyZh;$toir-RKDJ-D#3g=l=m&nNTnQ literal 60616 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 uSZ3KHD;0 Date: Sat, 14 Feb 2026 03:38:11 +0000 Subject: [PATCH 07/38] ci: authenticate model bootstrap downloads to avoid API rate limits --- .github/workflows/ci.yml | 6 ++++++ lexnlp/ml/catalog/download.py | 20 ++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50857a7..ce88a64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,8 @@ jobs: uv sync --frozen --python .venv/bin/python --extra dev --extra test - name: Bootstrap required assets + env: + GITHUB_TOKEN: ${{ github.token }} run: .venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model - name: Enforce skip-audit policy @@ -72,6 +74,8 @@ jobs: uv sync --frozen --python .venv/bin/python --extra dev --extra test - name: Bootstrap required assets (including Stanford) + env: + GITHUB_TOKEN: ${{ github.token }} run: .venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model --stanford - name: Run Stanford suite @@ -104,6 +108,8 @@ jobs: uv sync --frozen --python .venv/bin/python --extra test - name: Bootstrap contract-model asset + env: + GITHUB_TOKEN: ${{ github.token }} run: .venv/bin/python scripts/bootstrap_assets.py --contract-model - name: Run contract model quality gate diff --git a/lexnlp/ml/catalog/download.py b/lexnlp/ml/catalog/download.py index ab81e5b..cc2733e 100644 --- a/lexnlp/ml/catalog/download.py +++ b/lexnlp/ml/catalog/download.py @@ -11,6 +11,7 @@ # standard library import logging +import os from pathlib import Path from hashlib import md5 from base64 import b64encode @@ -38,6 +39,17 @@ class ChecksumError(Exception): pass +def _build_github_headers(headers: Dict[str, str]) -> Dict[str, str]: + """ + Augment request headers with an optional GitHub token for higher API limits. + """ + token: str = (os.getenv("GITHUB_TOKEN") or os.getenv("GH_TOKEN") or "").strip() + merged_headers: Dict[str, str] = dict(headers) + if token: + merged_headers["Authorization"] = f"Bearer {token}" + return merged_headers + + class GitHubReleaseDownloader: """ """ @@ -53,9 +65,9 @@ def download_release(cls, tag: str): def get_tag(tag: str) -> Response: response: Response = get( url=f'{MODELS_REPO}{tag}', - headers={ + headers=_build_github_headers({ 'Accept': 'application/vnd.github.v3+json', - }, + }), ) return response @@ -122,9 +134,9 @@ def download_asset( response: Response = get( url=asset['url'], stream=True, - headers={ + headers=_build_github_headers({ 'Accept': 'application/octet-stream', - }, + }), ) headers: CaseInsensitiveDict[str, Any] = response.headers name: str = asset.get('name') From fd735b6174bcc08de39c70f4e70b2cdbe9557754 Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 03:39:56 +0000 Subject: [PATCH 08/38] ci: bootstrap nltk before model-quality contract download --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce88a64..7541294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,7 +110,7 @@ jobs: - name: Bootstrap contract-model asset env: GITHUB_TOKEN: ${{ github.token }} - run: .venv/bin/python scripts/bootstrap_assets.py --contract-model + run: .venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model - name: Run contract model quality gate run: | From fa52427508e71c2c82f39d965cefaaadcb8cb867 Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 03:44:08 +0000 Subject: [PATCH 09/38] fix: stabilize German amount delimiter inference without locale packs --- lexnlp/utils/amount_delimiting.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lexnlp/utils/amount_delimiting.py b/lexnlp/utils/amount_delimiting.py index 96eae5f..61bdd07 100644 --- a/lexnlp/utils/amount_delimiting.py +++ b/lexnlp/utils/amount_delimiting.py @@ -169,6 +169,14 @@ def infer_delimiters( group_delimiter: str = locale_conventions['thousands_sep'] grouping: List[int] = locale_conventions['grouping'] + # Some runners do not have locale packs (e.g., de_DE.UTF-8) installed and + # silently fall back to "C"/"POSIX" numeric conventions. This breaks + # delimiter inference for values like "10.800" in German contexts. + if _locale.lower().startswith('de') and decimal_delimiter == '.' and not group_delimiter: + decimal_delimiter = ',' + group_delimiter = '.' + grouping = [3, 3, 0] + delimiters, blocks = get_delimited_blocks(text) len_delimiters: int = len(delimiters) From b07b9b6438bd95c65fdfe2752a17afb1d423e697 Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 04:01:18 +0000 Subject: [PATCH 10/38] fix: stabilize paragraph spans and de_DE delimiter fallback --- lexnlp/nlp/en/segments/paragraphs.py | 3 ++- lexnlp/utils/amount_delimiting.py | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lexnlp/nlp/en/segments/paragraphs.py b/lexnlp/nlp/en/segments/paragraphs.py index 839c76f..45d0049 100755 --- a/lexnlp/nlp/en/segments/paragraphs.py +++ b/lexnlp/nlp/en/segments/paragraphs.py @@ -161,7 +161,8 @@ def splitlines_with_spans(text: str) -> Tuple[List[str], List[Tuple[int, int]]]: spans: List[Tuple[int, int]] = [] if text is None: return lines, spans - last_line_end = -1 + # Start from offset 0 so single-line inputs without newlines keep full text. + last_line_end = 0 for m in RE_NEW_LINE.finditer(text): line = m.group('line') span = m.span() diff --git a/lexnlp/utils/amount_delimiting.py b/lexnlp/utils/amount_delimiting.py index 61bdd07..4d5b786 100644 --- a/lexnlp/utils/amount_delimiting.py +++ b/lexnlp/utils/amount_delimiting.py @@ -170,9 +170,14 @@ def infer_delimiters( grouping: List[int] = locale_conventions['grouping'] # Some runners do not have locale packs (e.g., de_DE.UTF-8) installed and - # silently fall back to "C"/"POSIX" numeric conventions. This breaks + # silently fall back to another locale (often C or en_US). This breaks # delimiter inference for values like "10.800" in German contexts. - if _locale.lower().startswith('de') and decimal_delimiter == '.' and not group_delimiter: + # de_DE is hardcoded in DE extractors, so enforce its canonical delimiters + # whenever locale resolution does not match those conventions. + if ( + _locale.lower().startswith('de_de') + and (decimal_delimiter != ',' or group_delimiter != '.') + ): decimal_delimiter = ',' group_delimiter = '.' grouping = [3, 3, 0] From d84f72e81e974841503f791dc7df018a6cdcd001 Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 04:20:47 +0000 Subject: [PATCH 11/38] docs: record final CI stabilization fixes --- notes.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/notes.md b/notes.md index 0ed9b5d..ed304db 100644 --- a/notes.md +++ b/notes.md @@ -90,6 +90,36 @@ Modernize dependency and test tooling so LexNLP is reproducible with `uv`, Pytho - `pytest lexnlp/extract/en/tests/test_dates.py lexnlp/extract/en/contracts/tests/test_contracts.py` - `11 passed` with reduced warning set. +## Final CI stabilization (post-migration) + +While validating the migrated stack on GitHub Actions, one base-suite run still failed +with two environment-sensitive issues. Both were fixed without adding skips/xfails. + +### 1) German money parsing on locale-limited runners +- Symptom: `lexnlp/extract/de/tests/test_money.py::TestMoneyPlain::test_symmetrical_money` + returned `Decimal('10.800')` instead of `Decimal('10800')` in CI. +- Root cause: `de_DE.UTF-8` was not always available; locale resolution could fall back + to non-German numeric conventions. +- Fix: hardened `lexnlp/utils/amount_delimiting.py` to enforce canonical `de_DE` + delimiters when locale resolution does not match `de_DE` conventions. + +### 2) Paragraph segmentation edge case for single-line input +- Symptom: `lexnlp/nlp/en/tests/test_paragraphs.py::TestParagraphs::test_date_text` + intermittently returned `'6'` instead of the full timestamp text. +- Root cause: `splitlines_with_spans()` initialized `last_line_end` to `-1`, so + no-newline input could be sliced from the final character depending on model output. +- Fix: updated `lexnlp/nlp/en/segments/paragraphs.py` to initialize `last_line_end` + at `0`, preserving full single-line input spans. + +### CI verification after these fixes +- Commit: `b07b9b6` +- Run: `https://github.com/5pence5/lexpredict-lexnlp_fixed/actions/runs/22010774834` +- Result: all jobs green + - `Base Tests` + - `Stanford Tests` + - `Model Quality Gate` + - `Packaging Smoke` + ## Operational guidance Reliable full-validation flow on this machine: From d878e429309dca05c8fd1436b7db4faee6ce089f Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 14:51:49 +0000 Subject: [PATCH 12/38] feat: add contract-model training workflow and tag overrides --- MIGRATION_RUNBOOK.md | 37 ++ lexnlp/extract/en/contracts/predictors.py | 2 + .../tests/test_model_tag_overrides.py | 24 + lexnlp/ml/predictor.py | 19 +- scripts/bootstrap_assets.py | 40 +- scripts/model_quality_gate.py | 11 +- scripts/reexport_contract_model.py | 11 +- scripts/train_contract_model.py | 509 ++++++++++++++++++ 8 files changed, 643 insertions(+), 10 deletions(-) create mode 100644 lexnlp/extract/en/contracts/tests/test_model_tag_overrides.py create mode 100644 scripts/train_contract_model.py diff --git a/MIGRATION_RUNBOOK.md b/MIGRATION_RUNBOOK.md index 2784aeb..d247997 100644 --- a/MIGRATION_RUNBOOK.md +++ b/MIGRATION_RUNBOOK.md @@ -89,6 +89,31 @@ under the current runtime (Python/scikit-learn), use: This writes model-export metadata to `artifacts/model_reexports/pipeline__is-contract__0.2.metadata.json` by default. +### Retrain candidate classifier from corpora (phase 2 path) + +For a fuller upgrade than pure re-serialization, train a new classifier while +reusing LexNLP baseline preprocessing/vectorization steps: + +```bash +./.venv/bin/python scripts/train_contract_model.py \ + --baseline-tag pipeline/is-contract/0.1 \ + --candidate-tag pipeline/is-contract/0.2 \ + --baseline-metrics-json test_data/model_quality/is_contract_baseline_metrics.json \ + --max-f1-regression 0.0 \ + --max-accuracy-regression 0.0 \ + --force +``` + +Training report output: +- `artifacts/model_training/contract_model_training_report.json` + +The script automatically: +- downloads configured corpora tags if missing, +- trains multiple estimator candidates, +- selects best by validation metrics (F1 first), +- writes candidate artifact to catalog, +- runs `scripts/model_quality_gate.py` unless skipped. + Candidate evaluation command: ```bash @@ -104,6 +129,18 @@ Candidate evaluation command: Default policy is non-regression against baseline metrics from `pipeline/is-contract/0.1` on the fixed fixture above. +### Runtime model-tag overrides + +Predictors can select newer validated tags without API/signature changes: + +```bash +# is-contract classifier +export LEXNLP_IS_CONTRACT_MODEL_TAG="pipeline/is-contract/0.2" + +# contract-type classifier +export LEXNLP_CONTRACT_TYPE_MODEL_TAG="pipeline/contract-type/0.2" +``` + If baseline-tag model behavior is intentionally changed, regenerate and review the baseline metrics file in the same PR: diff --git a/lexnlp/extract/en/contracts/predictors.py b/lexnlp/extract/en/contracts/predictors.py index 0e5dff5..675e9b6 100644 --- a/lexnlp/extract/en/contracts/predictors.py +++ b/lexnlp/extract/en/contracts/predictors.py @@ -26,6 +26,7 @@ class ProbabilityPredictorIsContract(ProbabilityPredictor): """ _DEFAULT_PIPELINE: str = 'pipeline/is-contract/0.1' + _DEFAULT_PIPELINE_ENV_VAR: str = 'LEXNLP_IS_CONTRACT_MODEL_TAG' def _sanity_check(self) -> None: """ @@ -72,6 +73,7 @@ class ProbabilityPredictorContractType(ProbabilityPredictor): """ _DEFAULT_PIPELINE: str = 'pipeline/contract-type/0.1' + _DEFAULT_PIPELINE_ENV_VAR: str = 'LEXNLP_CONTRACT_TYPE_MODEL_TAG' def _sanity_check(self) -> None: """ diff --git a/lexnlp/extract/en/contracts/tests/test_model_tag_overrides.py b/lexnlp/extract/en/contracts/tests/test_model_tag_overrides.py new file mode 100644 index 0000000..ac6528c --- /dev/null +++ b/lexnlp/extract/en/contracts/tests/test_model_tag_overrides.py @@ -0,0 +1,24 @@ +from lexnlp.extract.en.contracts.predictors import ( + ProbabilityPredictorContractType, + ProbabilityPredictorIsContract, +) + + +def test_is_contract_default_pipeline_tag_no_env(monkeypatch): + monkeypatch.delenv("LEXNLP_IS_CONTRACT_MODEL_TAG", raising=False) + assert ProbabilityPredictorIsContract.get_default_pipeline_tag() == "pipeline/is-contract/0.1" + + +def test_is_contract_default_pipeline_tag_with_env(monkeypatch): + monkeypatch.setenv("LEXNLP_IS_CONTRACT_MODEL_TAG", "pipeline/is-contract/0.2") + assert ProbabilityPredictorIsContract.get_default_pipeline_tag() == "pipeline/is-contract/0.2" + + +def test_contract_type_default_pipeline_tag_no_env(monkeypatch): + monkeypatch.delenv("LEXNLP_CONTRACT_TYPE_MODEL_TAG", raising=False) + assert ProbabilityPredictorContractType.get_default_pipeline_tag() == "pipeline/contract-type/0.1" + + +def test_contract_type_default_pipeline_tag_with_env(monkeypatch): + monkeypatch.setenv("LEXNLP_CONTRACT_TYPE_MODEL_TAG", "pipeline/contract-type/0.2") + assert ProbabilityPredictorContractType.get_default_pipeline_tag() == "pipeline/contract-type/0.2" diff --git a/lexnlp/ml/predictor.py b/lexnlp/ml/predictor.py index 4077818..540f42a 100644 --- a/lexnlp/ml/predictor.py +++ b/lexnlp/ml/predictor.py @@ -10,6 +10,7 @@ # standard library +import os from pathlib import Path from abc import ABC, abstractmethod from typing import Any, Optional, Protocol, runtime_checkable @@ -56,6 +57,7 @@ class and abstract methods. """ _DEFAULT_PIPELINE: str = NotImplemented + _DEFAULT_PIPELINE_ENV_VAR: Optional[str] = None def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @@ -117,6 +119,21 @@ def _sanity_check(self) -> None: """ raise NotImplementedError + @classmethod + def get_default_pipeline_tag(cls) -> str: + """ + Resolve the pipeline tag for this predictor class. + + Returns: + Pipeline catalog tag, optionally overridden by an environment variable. + """ + env_var = cls._DEFAULT_PIPELINE_ENV_VAR + if env_var: + value = os.getenv(env_var, "").strip() + if value: + return value + return cls._DEFAULT_PIPELINE + @classmethod def get_default_pipeline(cls) -> Pipeline: """ @@ -125,6 +142,6 @@ def get_default_pipeline(cls) -> Pipeline: Returns: A default Scikit-Learn Pipeline for usage with this ProbabilityPredictor. """ - path: Path = get_path_from_catalog(cls._DEFAULT_PIPELINE) + path: Path = get_path_from_catalog(cls.get_default_pipeline_tag()) with open(path, 'rb') as f: return load(f) diff --git a/scripts/bootstrap_assets.py b/scripts/bootstrap_assets.py index 78e8225..def0313 100755 --- a/scripts/bootstrap_assets.py +++ b/scripts/bootstrap_assets.py @@ -5,6 +5,7 @@ import argparse import logging +import os import sys import zipfile from pathlib import Path @@ -27,7 +28,19 @@ ) OPTIONAL_NLTK_RESOURCES = ("punkt_tab",) -CONTRACT_MODEL_TAG = "pipeline/is-contract/0.1" + +def resolve_contract_model_tag() -> str: + """ + Resolve the contract-model tag from env overrides with backward compatibility. + """ + return ( + os.getenv("LEXNLP_CONTRACT_MODEL_TAG") + or os.getenv("LEXNLP_IS_CONTRACT_MODEL_TAG") + or "pipeline/is-contract/0.1" + ).strip() + + +CONTRACT_MODEL_TAG = resolve_contract_model_tag() STANFORD_DOWNLOADS: Tuple[Tuple[str, str, Tuple[str, ...]], ...] = ( ( @@ -77,7 +90,11 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: parser.add_argument( "--contract-model", action="store_true", - help="Download LexNLP contract model release pipeline/is-contract/0.1.", + help=( + "Download LexNLP contract model release. " + "Respects env overrides LEXNLP_CONTRACT_MODEL_TAG / " + "LEXNLP_IS_CONTRACT_MODEL_TAG." + ), ) parser.add_argument( "--stanford", @@ -254,9 +271,9 @@ def bootstrap_nltk(*, dry_run: bool) -> None: LOGGER.warning("Optional NLTK resource unavailable: %s", resource) -def bootstrap_contract_model(*, dry_run: bool) -> None: +def bootstrap_contract_model(*, dry_run: bool, tag: str) -> None: if dry_run: - LOGGER.info("DRY RUN: would download LexNLP model tag %s", CONTRACT_MODEL_TAG) + LOGGER.info("DRY RUN: would download LexNLP model tag %s", tag) return try: @@ -266,8 +283,8 @@ def bootstrap_contract_model(*, dry_run: bool) -> None: "Unable to import LexNLP catalog downloader. Ensure dependencies and editable install are in place." ) from error - LOGGER.info("Downloading LexNLP contract model: %s", CONTRACT_MODEL_TAG) - download_github_release(CONTRACT_MODEL_TAG, prompt_user=False) + LOGGER.info("Downloading LexNLP contract model: %s", tag) + download_github_release(tag, prompt_user=False) def run_selected_tasks(args: argparse.Namespace) -> None: @@ -280,7 +297,16 @@ def run_selected_tasks(args: argparse.Namespace) -> None: if run_nltk: tasks.append(("nltk", lambda: bootstrap_nltk(dry_run=args.dry_run))) if run_contract_model: - tasks.append(("contract-model", lambda: bootstrap_contract_model(dry_run=args.dry_run))) + contract_model_tag = resolve_contract_model_tag() + tasks.append( + ( + "contract-model", + lambda: bootstrap_contract_model( + dry_run=args.dry_run, + tag=contract_model_tag, + ), + ) + ) if run_stanford: stanford_dir = Path(args.stanford_dir).expanduser().resolve() tasks.append( diff --git a/scripts/model_quality_gate.py b/scripts/model_quality_gate.py index e4ebe40..3a4a888 100644 --- a/scripts/model_quality_gate.py +++ b/scripts/model_quality_gate.py @@ -6,6 +6,7 @@ import argparse import csv import json +import os import sys from pathlib import Path from typing import Any, Dict, List, Sequence, Tuple @@ -17,13 +18,21 @@ REQUIRED_METRIC_KEYS = ("accuracy", "f1", "precision", "recall") +def resolve_contract_model_tag() -> str: + return ( + os.getenv("LEXNLP_CONTRACT_MODEL_TAG") + or os.getenv("LEXNLP_IS_CONTRACT_MODEL_TAG") + or "pipeline/is-contract/0.1" + ).strip() + + def parse_args(argv: Sequence[str]) -> argparse.Namespace: parser = argparse.ArgumentParser( description="Compare baseline and candidate contract models on a labeled fixture.", ) parser.add_argument( "--baseline-tag", - default="pipeline/is-contract/0.1", + default=resolve_contract_model_tag(), help="Catalog tag used as baseline model.", ) parser.add_argument( diff --git a/scripts/reexport_contract_model.py b/scripts/reexport_contract_model.py index 7fcec4f..47421a9 100644 --- a/scripts/reexport_contract_model.py +++ b/scripts/reexport_contract_model.py @@ -5,6 +5,7 @@ import argparse import json +import os import pickle import subprocess import sys @@ -24,6 +25,14 @@ LEGACY_WARNING_TOKEN = "Trying to unpickle estimator" +def resolve_contract_model_tag() -> str: + return ( + os.getenv("LEXNLP_CONTRACT_MODEL_TAG") + or os.getenv("LEXNLP_IS_CONTRACT_MODEL_TAG") + or "pipeline/is-contract/0.1" + ).strip() + + def parse_args(argv: Sequence[str]) -> argparse.Namespace: parser = argparse.ArgumentParser( description=( @@ -33,7 +42,7 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: ) parser.add_argument( "--source-tag", - default="pipeline/is-contract/0.1", + default=resolve_contract_model_tag(), help="Source model tag to load from LexNLP catalog.", ) parser.add_argument( diff --git a/scripts/train_contract_model.py b/scripts/train_contract_model.py new file mode 100644 index 0000000..87795a8 --- /dev/null +++ b/scripts/train_contract_model.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python3 +"""Train and validate a contract-classifier candidate from released corpora.""" + +from __future__ import annotations + +import argparse +import copy +import json +import pickle +import subprocess +import sys +import tarfile +from pathlib import Path +from typing import Dict, Iterable, List, Mapping, Sequence, Tuple + +from cloudpickle import load +from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score +from sklearn.model_selection import train_test_split +from sklearn.naive_bayes import GaussianNB +from sklearn.pipeline import Pipeline + + +DEFAULT_POSITIVE_TAGS: Tuple[str, ...] = ( + "corpus/contract-types/0.1", + "corpus/atticus-cuad-v1-plaintext/0.1", +) +DEFAULT_NEGATIVE_TAGS: Tuple[str, ...] = ( + "corpus/uspto-sample/0.1", + "corpus/sec-edgar-forms-3-4-5-8k-10k-sample/0.1", + "corpus/arxiv-abstracts-with-agreement/0.1", + "corpus/eurlex-sample-10000/0.1", +) +DEFAULT_BASELINE_METRICS = Path("test_data/model_quality/is_contract_baseline_metrics.json") +DEFAULT_FIXTURE = Path( + "test_data/lexnlp/extract/en/contracts/tests/test_contracts/test_is_contract.csv" +) + + +class TrainingError(Exception): + """Raised for unrecoverable training pipeline errors.""" + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Train a contract-model candidate by reusing LexNLP baseline preprocessing/" + "vectorization steps and replacing the classifier." + ) + ) + parser.add_argument( + "--baseline-tag", + default="pipeline/is-contract/0.1", + help="Existing pipeline tag used as feature-extractor baseline.", + ) + parser.add_argument( + "--candidate-tag", + default="pipeline/is-contract/0.2", + help="Catalog tag where the candidate artifact will be written.", + ) + parser.add_argument( + "--positive-tags", + nargs="+", + default=list(DEFAULT_POSITIVE_TAGS), + help="Corpus tags labeled as contracts.", + ) + parser.add_argument( + "--negative-tags", + nargs="+", + default=list(DEFAULT_NEGATIVE_TAGS), + help="Corpus tags labeled as non-contracts.", + ) + parser.add_argument( + "--max-docs-per-tag", + type=int, + default=800, + help="Maximum documents extracted from each corpus tag.", + ) + parser.add_argument( + "--head-character-n", + type=int, + default=4000, + help="Maximum characters read from each document.", + ) + parser.add_argument( + "--validation-size", + type=float, + default=0.2, + help="Validation split ratio for estimator selection.", + ) + parser.add_argument( + "--random-state", + type=int, + default=7, + help="Random seed for train/validation split and estimators.", + ) + parser.add_argument( + "--estimators", + nargs="+", + default=("gaussian_nb", "logistic_regression", "random_forest"), + choices=("gaussian_nb", "logistic_regression", "random_forest"), + help="Estimator candidates to train and compare.", + ) + parser.add_argument( + "--max-workers", + type=int, + default=4, + help="Worker count for random-forest training.", + ) + parser.add_argument( + "--min-probability", + type=float, + default=0.3, + help="Probability threshold used for metrics and quality gate.", + ) + parser.add_argument( + "--max-accuracy-regression", + type=float, + default=0.0, + help="Quality gate max allowed candidate accuracy drop.", + ) + parser.add_argument( + "--max-f1-regression", + type=float, + default=0.0, + help="Quality gate max allowed candidate F1 drop.", + ) + parser.add_argument( + "--baseline-metrics-json", + type=Path, + default=DEFAULT_BASELINE_METRICS, + help=( + "Committed baseline metrics JSON consumed by quality gate " + f"(default: {DEFAULT_BASELINE_METRICS})." + ), + ) + parser.add_argument( + "--fixture", + type=Path, + default=DEFAULT_FIXTURE, + help=f"Fixed evaluation fixture for quality gate (default: {DEFAULT_FIXTURE}).", + ) + parser.add_argument( + "--output-json", + type=Path, + default=Path("artifacts/model_training/contract_model_training_report.json"), + help="Path for detailed training report JSON.", + ) + parser.add_argument( + "--skip-quality-gate", + action="store_true", + help="Skip model_quality_gate.py validation.", + ) + parser.add_argument( + "--keep-candidate-on-failure", + action="store_true", + help="Keep candidate artifact even if quality gate fails.", + ) + parser.add_argument( + "--force", + action="store_true", + help="Overwrite candidate artifact if it already exists.", + ) + + args = parser.parse_args(argv) + if args.max_docs_per_tag <= 0: + parser.error("--max-docs-per-tag must be > 0") + if args.head_character_n <= 0: + parser.error("--head-character-n must be > 0") + if not (0.05 <= args.validation_size <= 0.5): + parser.error("--validation-size must be between 0.05 and 0.5") + if args.max_workers <= 0: + parser.error("--max-workers must be > 0") + if not args.positive_tags: + parser.error("--positive-tags must not be empty") + if not args.negative_tags: + parser.error("--negative-tags must not be empty") + return args + + +def ensure_tag_downloaded(tag: str) -> Path: + from lexnlp.ml.catalog import get_path_from_catalog + from lexnlp.ml.catalog.download import download_github_release + + try: + return get_path_from_catalog(tag) + except FileNotFoundError: + download_github_release(tag, prompt_user=False) + return get_path_from_catalog(tag) + + +def patch_legacy_estimator_attributes(pipeline: Pipeline) -> None: + estimator = pipeline._final_estimator + if hasattr(estimator, "sigma_"): + if not hasattr(estimator, "var_"): + estimator.var_ = estimator.sigma_ + if not hasattr(estimator, "variance_"): + estimator.variance_ = estimator.var_ + + for _, _, transform in pipeline._iter(with_final=False): + transform.clip = hasattr(transform, "clip") and transform.clip + + +def load_pipeline_for_tag(tag: str) -> Tuple[Path, Pipeline]: + path = ensure_tag_downloaded(tag) + with path.open("rb") as model_file: + pipeline = load(model_file) + patch_legacy_estimator_attributes(pipeline) + return path, pipeline + + +def iter_texts_from_archive(path: Path, *, max_docs: int, head_character_n: int) -> Iterable[str]: + yielded = 0 + with tarfile.open(path, mode="r:*") as archive: + for member in archive: + if yielded >= max_docs: + break + if not member.isfile() or not member.name.lower().endswith(".txt"): + continue + file_obj = archive.extractfile(member) + if file_obj is None: + continue + payload = file_obj.read(head_character_n * 2) + text = payload.decode("utf-8", errors="ignore").strip() + if not text: + continue + yield text[:head_character_n] + yielded += 1 + + +def collect_corpus_samples( + tags: Iterable[str], + *, + label: bool, + max_docs_per_tag: int, + head_character_n: int, +) -> Tuple[List[str], List[bool], Dict[str, int]]: + texts: List[str] = [] + labels: List[bool] = [] + counts: Dict[str, int] = {} + + for tag in tags: + archive_path = ensure_tag_downloaded(tag) + extracted = list( + iter_texts_from_archive( + archive_path, + max_docs=max_docs_per_tag, + head_character_n=head_character_n, + ) + ) + if not extracted: + raise TrainingError(f"No text samples extracted from {tag} ({archive_path})") + texts.extend(extracted) + labels.extend([label] * len(extracted)) + counts[tag] = len(extracted) + return texts, labels, counts + + +def make_estimator(name: str, *, random_state: int, max_workers: int): + if name == "gaussian_nb": + return GaussianNB() + if name == "logistic_regression": + return LogisticRegression( + class_weight="balanced", + max_iter=600, + random_state=random_state, + ) + if name == "random_forest": + return RandomForestClassifier( + n_estimators=300, + class_weight="balanced_subsample", + min_samples_leaf=2, + random_state=random_state, + n_jobs=max_workers, + ) + raise ValueError(f"Unsupported estimator: {name}") + + +def build_candidate_pipeline(feature_steps: Sequence[Tuple[str, object]], estimator_name: str, *, random_state: int, max_workers: int) -> Pipeline: + steps = [(name, copy.deepcopy(step)) for name, step in feature_steps] + steps.append((estimator_name, make_estimator(estimator_name, random_state=random_state, max_workers=max_workers))) + return Pipeline(steps=steps) + + +def score_pipeline( + pipeline: Pipeline, + texts: Sequence[str], + labels: Sequence[bool], + *, + min_probability: float, +) -> Dict[str, float]: + probabilities = pipeline.predict_proba(texts)[:, 1] + predictions = probabilities >= min_probability + return { + "accuracy": float(accuracy_score(labels, predictions)), + "f1": float(f1_score(labels, predictions)), + "precision": float(precision_score(labels, predictions, zero_division=0)), + "recall": float(recall_score(labels, predictions, zero_division=0)), + } + + +def choose_best(scores: Mapping[str, Dict[str, float]]) -> str: + ranked = sorted( + scores.items(), + key=lambda item: ( + item[1]["f1"], + item[1]["accuracy"], + item[1]["precision"], + item[1]["recall"], + ), + reverse=True, + ) + return ranked[0][0] + + +def write_candidate_to_catalog( + *, + baseline_model_path: Path, + candidate_tag: str, + pipeline: Pipeline, + force: bool, +) -> Path: + from lexnlp.ml.catalog import CATALOG + + destination_dir = CATALOG / candidate_tag + destination_dir.mkdir(parents=True, exist_ok=True) + destination_path = destination_dir / baseline_model_path.name + + if destination_path.exists() and not force: + raise FileExistsError( + f"Candidate path already exists: {destination_path}. Pass --force to overwrite." + ) + + with destination_path.open("wb") as candidate_file: + pickle.dump(pipeline, candidate_file) + return destination_path + + +def run_quality_gate( + *, + baseline_tag: str, + candidate_tag: str, + fixture: Path, + baseline_metrics_json: Path, + min_probability: float, + max_accuracy_regression: float, + max_f1_regression: float, +) -> None: + gate_script = Path(__file__).with_name("model_quality_gate.py") + cmd = [ + sys.executable, + str(gate_script), + "--baseline-tag", + baseline_tag, + "--candidate-tag", + candidate_tag, + "--fixture", + str(fixture), + "--min-probability", + str(min_probability), + "--max-accuracy-regression", + str(max_accuracy_regression), + "--max-f1-regression", + str(max_f1_regression), + ] + + if baseline_metrics_json.exists(): + cmd.extend(["--baseline-metrics-json", str(baseline_metrics_json)]) + + subprocess.run(cmd, check=True) + + +def main(argv: Sequence[str]) -> int: + args = parse_args(argv) + + baseline_model_path, baseline_pipeline = load_pipeline_for_tag(args.baseline_tag) + feature_steps = baseline_pipeline.steps[:-1] + if not feature_steps: + raise TrainingError("Baseline pipeline has no feature steps to reuse.") + + positive_texts, positive_labels, positive_counts = collect_corpus_samples( + args.positive_tags, + label=True, + max_docs_per_tag=args.max_docs_per_tag, + head_character_n=args.head_character_n, + ) + negative_texts, negative_labels, negative_counts = collect_corpus_samples( + args.negative_tags, + label=False, + max_docs_per_tag=args.max_docs_per_tag, + head_character_n=args.head_character_n, + ) + + texts = positive_texts + negative_texts + labels = positive_labels + negative_labels + + X_train, X_val, y_train, y_val = train_test_split( + texts, + labels, + test_size=args.validation_size, + random_state=args.random_state, + shuffle=True, + stratify=labels, + ) + + estimator_scores: Dict[str, Dict[str, float]] = {} + fitted_pipelines: Dict[str, Pipeline] = {} + for estimator_name in args.estimators: + candidate = build_candidate_pipeline( + feature_steps, + estimator_name, + random_state=args.random_state, + max_workers=args.max_workers, + ) + candidate.fit(X_train, y_train) + patch_legacy_estimator_attributes(candidate) + estimator_scores[estimator_name] = score_pipeline( + candidate, + X_val, + y_val, + min_probability=args.min_probability, + ) + fitted_pipelines[estimator_name] = candidate + + selected_estimator = choose_best(estimator_scores) + + # Refit the selected estimator on the full dataset before writing candidate. + selected_pipeline = build_candidate_pipeline( + feature_steps, + selected_estimator, + random_state=args.random_state, + max_workers=args.max_workers, + ) + selected_pipeline.fit(texts, labels) + patch_legacy_estimator_attributes(selected_pipeline) + + candidate_model_path = write_candidate_to_catalog( + baseline_model_path=baseline_model_path, + candidate_tag=args.candidate_tag, + pipeline=selected_pipeline, + force=args.force, + ) + + report = { + "baseline_tag": args.baseline_tag, + "candidate_tag": args.candidate_tag, + "baseline_model_path": str(baseline_model_path), + "candidate_model_path": str(candidate_model_path), + "selected_estimator": selected_estimator, + "estimators": estimator_scores, + "dataset": { + "positive_counts": positive_counts, + "negative_counts": negative_counts, + "total_samples": len(texts), + "train_samples": len(X_train), + "validation_samples": len(X_val), + "max_docs_per_tag": args.max_docs_per_tag, + "head_character_n": args.head_character_n, + }, + "validation_baseline": score_pipeline( + baseline_pipeline, + X_val, + y_val, + min_probability=args.min_probability, + ), + "validation_candidate_selected": score_pipeline( + selected_pipeline, + X_val, + y_val, + min_probability=args.min_probability, + ), + "quality_gate": { + "skipped": bool(args.skip_quality_gate), + "status": "not-run" if args.skip_quality_gate else "pending", + }, + } + + if args.skip_quality_gate: + report["quality_gate"]["status"] = "skipped" + else: + try: + run_quality_gate( + baseline_tag=args.baseline_tag, + candidate_tag=args.candidate_tag, + fixture=args.fixture, + baseline_metrics_json=args.baseline_metrics_json, + min_probability=args.min_probability, + max_accuracy_regression=args.max_accuracy_regression, + max_f1_regression=args.max_f1_regression, + ) + report["quality_gate"]["status"] = "passed" + except subprocess.CalledProcessError: + report["quality_gate"]["status"] = "failed" + if not args.keep_candidate_on_failure and candidate_model_path.exists(): + candidate_model_path.unlink() + report["quality_gate"]["candidate_removed"] = True + args.output_json.parent.mkdir(parents=True, exist_ok=True) + args.output_json.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8") + return 1 + + args.output_json.parent.mkdir(parents=True, exist_ok=True) + args.output_json.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8") + print(json.dumps(report, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) From 2e0a59c37ba72d2d66aca9f69cc6a18723c40f26 Mon Sep 17 00:00:00 2001 From: Jack Eames Date: Sat, 14 Feb 2026 15:18:18 +0000 Subject: [PATCH 13/38] feat: add runtime contract-type model fallback and CI smoke --- .github/workflows/ci.yml | 42 ++++ AGENTS.md | 7 +- MIGRATION_RUNBOOK.md | 29 ++- lexnlp/extract/en/contracts/predictors.py | 25 +++ lexnlp/extract/en/contracts/runtime_model.py | 187 ++++++++++++++++++ .../tests/test_model_tag_overrides.py | 59 ++++++ notes.md | 19 ++ scripts/bootstrap_assets.py | 66 ++++++- scripts/train_contract_type_model.py | 155 +++++++++++++++ 9 files changed, 584 insertions(+), 5 deletions(-) create mode 100644 lexnlp/extract/en/contracts/runtime_model.py create mode 100644 scripts/train_contract_type_model.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7541294..77267f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ env: CONTRACT_MODEL_BASELINE_TAG: "pipeline/is-contract/0.1" CONTRACT_MODEL_CANDIDATE_TAG: "pipeline/is-contract/0.1" CONTRACT_MODEL_BASELINE_METRICS: "test_data/model_quality/is_contract_baseline_metrics.json" + CONTRACT_TYPE_MODEL_TAG: "pipeline/contract-type/0.2-runtime" jobs: base-tests: @@ -129,6 +130,47 @@ jobs: path: artifacts/model_quality_gate.json if-no-files-found: error + contract-type-smoke: + name: Contract Type Smoke + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + LEXNLP_CONTRACT_TYPE_MODEL_TAG: ${{ env.CONTRACT_TYPE_MODEL_TAG }} + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Install dependencies + run: | + uv venv .venv --python "${PYTHON_VERSION}" + uv sync --frozen --python .venv/bin/python --extra test + + - name: Bootstrap runtime contract-type model + env: + GITHUB_TOKEN: ${{ github.token }} + run: .venv/bin/python scripts/bootstrap_assets.py --contract-type-model + + - name: Run contract-type predictor smoke + run: | + .venv/bin/python - <<'PY' + from lexnlp.extract.en.contracts.predictors import ProbabilityPredictorContractType + predictor = ProbabilityPredictorContractType() + predictions = predictor.make_predictions( + "This Employment Agreement is entered into on January 1, 2024.", + top_n=3, + ) + assert len(predictions) > 0 + print(predictions.to_dict()) + PY + packaging-smoke: name: Packaging Smoke runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index b3d0167..362bcaa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,7 @@ uv pip install --python .venv/bin/python -e ".[dev,test]" Use the bootstrap script for deterministic setup: ```bash -./.venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model +./.venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model --contract-type-model ``` Optional assets: @@ -148,6 +148,10 @@ python3 ci/check_dist_contents.py --candidate-tag pipeline/is-contract/0.1 \ --baseline-metrics-json test_data/model_quality/is_contract_baseline_metrics.json +# build a runtime-compatible contract-type model artifact +./.venv/bin/python scripts/train_contract_type_model.py \ + --target-tag pipeline/contract-type/0.2-runtime + # create a re-exported candidate model tag and validate it ./.venv/bin/python scripts/reexport_contract_model.py \ --source-tag pipeline/is-contract/0.1 \ @@ -176,5 +180,6 @@ python3 ci/check_dist_contents.py - Full base run (`pytest lexnlp`) passes. - If Stanford assets are enabled, Stanford-only suite with `LEXNLP_USE_STANFORD=true` passes. - Contract model quality gate passes against `test_data/model_quality/is_contract_baseline_metrics.json`. +- Contract-type smoke flow works (`scripts/bootstrap_assets.py --contract-type-model` + predictor instantiation). - No `skip`/`skipif`/`xfail` policy bypasses were introduced. - Document any required asset downloads (NLTK, pipeline models, Stanford, Tika) in PR notes. diff --git a/MIGRATION_RUNBOOK.md b/MIGRATION_RUNBOOK.md index d247997..04c217e 100644 --- a/MIGRATION_RUNBOOK.md +++ b/MIGRATION_RUNBOOK.md @@ -26,8 +26,8 @@ uv pip install --python .venv/bin/python -e ".[dev,test]" ## 3) Bootstrap Required Assets ```bash -# NLTK + required model artifact -./.venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model +# NLTK + required model artifacts +./.venv/bin/python scripts/bootstrap_assets.py --nltk --contract-model --contract-type-model # Optional: Stanford assets for Stanford-gated tests ./.venv/bin/python scripts/bootstrap_assets.py --stanford @@ -141,6 +141,31 @@ export LEXNLP_IS_CONTRACT_MODEL_TAG="pipeline/is-contract/0.2" export LEXNLP_CONTRACT_TYPE_MODEL_TAG="pipeline/contract-type/0.2" ``` +### Contract-type runtime fallback model + +The legacy `pipeline/contract-type/0.1` artifact may fail to unpickle on modern +Python runtimes. LexNLP now supports a deterministic runtime-compatible fallback +artifact (`pipeline/contract-type/0.2-runtime`) trained from +`corpus/contract-types/0.1`. + +Build/rebuild it explicitly: + +```bash +./.venv/bin/python scripts/bootstrap_assets.py --contract-type-model +``` + +Or run full training with report output: + +```bash +./.venv/bin/python scripts/train_contract_type_model.py \ + --target-tag pipeline/contract-type/0.2-runtime \ + --output-json artifacts/model_training/contract_type_model_training_report.json +``` + +On first use of `ProbabilityPredictorContractType`, if legacy default loading +fails and no env override is set, LexNLP automatically builds/loads this runtime +fallback tag. + If baseline-tag model behavior is intentionally changed, regenerate and review the baseline metrics file in the same PR: diff --git a/lexnlp/extract/en/contracts/predictors.py b/lexnlp/extract/en/contracts/predictors.py index 675e9b6..9871d43 100644 --- a/lexnlp/extract/en/contracts/predictors.py +++ b/lexnlp/extract/en/contracts/predictors.py @@ -74,12 +74,37 @@ class ProbabilityPredictorContractType(ProbabilityPredictor): _DEFAULT_PIPELINE: str = 'pipeline/contract-type/0.1' _DEFAULT_PIPELINE_ENV_VAR: str = 'LEXNLP_CONTRACT_TYPE_MODEL_TAG' + _RUNTIME_FALLBACK_PIPELINE: str = 'pipeline/contract-type/0.2-runtime' def _sanity_check(self) -> None: """ Does nothing. No sanity check required. """ + @classmethod + def get_default_pipeline(cls): + try: + return super().get_default_pipeline() + except Exception: + # Respect explicit env override failures and only auto-fallback for + # the legacy default model tag. + if cls.get_default_pipeline_tag() != cls._DEFAULT_PIPELINE: + raise + + from lexnlp.extract.en.contracts.runtime_model import ( + ensure_runtime_contract_type_model, + load_pipeline_for_tag, + ) + + ensure_runtime_contract_type_model(target_tag=cls._RUNTIME_FALLBACK_PIPELINE) + try: + return load_pipeline_for_tag(cls._RUNTIME_FALLBACK_PIPELINE) + except Exception as fallback_error: + raise RuntimeError( + "Failed to load legacy contract-type model and runtime fallback model. " + "Run `python scripts/bootstrap_assets.py --contract-type-model` and retry." + ) from fallback_error + def make_predictions( self, text: Union[str, Iterable[str]], diff --git a/lexnlp/extract/en/contracts/runtime_model.py b/lexnlp/extract/en/contracts/runtime_model.py new file mode 100644 index 0000000..9db7966 --- /dev/null +++ b/lexnlp/extract/en/contracts/runtime_model.py @@ -0,0 +1,187 @@ +""" +Utilities to build and load a Python 3.11-compatible contract-type classifier. +""" + +__author__ = "ContraxSuite, LLC; LexPredict, LLC" +__copyright__ = "Copyright 2015-2021, ContraxSuite, LLC" +__license__ = "https://github.com/LexPredict/lexpredict-lexnlp/blob/2.3.0/LICENSE" +__version__ = "2.3.0" +__maintainer__ = "LexPredict, LLC" +__email__ = "support@contraxsuite.com" + + +# standard library +import pickle +import tarfile +from collections import defaultdict +from pathlib import Path +from typing import Dict, Iterable, List, Sequence, Tuple + +# third-party imports +from cloudpickle import load +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.linear_model import LogisticRegression +from sklearn.pipeline import Pipeline + + +LEGACY_CONTRACT_TYPE_TAG = "pipeline/contract-type/0.1" +RUNTIME_CONTRACT_TYPE_TAG = "pipeline/contract-type/0.2-runtime" +CONTRACT_TYPE_CORPUS_TAG = "corpus/contract-types/0.1" +CONTRACT_TYPE_MODEL_FILENAME = "pipeline_contract_type_classifier.cloudpickle" + + +def ensure_tag_downloaded(tag: str) -> Path: + from lexnlp.ml.catalog import get_path_from_catalog + from lexnlp.ml.catalog.download import download_github_release + + try: + return get_path_from_catalog(tag) + except FileNotFoundError: + download_github_release(tag, prompt_user=False) + return get_path_from_catalog(tag) + + +def load_pipeline_for_tag(tag: str) -> Pipeline: + path = ensure_tag_downloaded(tag) + with path.open("rb") as model_file: + return load(model_file) + + +def _extract_label(member_name: str) -> str: + # Expected shape: CONTRACT_TYPES/